⌘ kean.blog

Three Use Cases of Phantom Types

  

Phantom types are used to parameterize generic types but never actually appear in their implementation. Why are they useful? For additional type safety.

With phantom types, you can add extra information to your types and use it to restrict the code to make invalid situations impossible. Having more restrictions means less room for programming error, better, self-documenting code, and fewer tests. In practice, those restrictions can be quite complex which I would demonstrate on the three examples from my most recent projects:

Id Type #

The Id type represents an identifier of a model entity. It gets parametrized with an Entity which is used by the compiler when comparing different types of Ids. This way the compiler prevents you from accidentally mixing up different ids. Essentially each entity has its own type of id (e.g. Id<User>, Id<Image>, etc).

struct Id<Entity>: Hashable {
    let raw: String
        
    var hashValue: Int {
        return raw.hashValue
    }
    
    static func ==(lhs: Id, rhs: Id) -> Bool {
        return lhs.raw == rhs.raw
    }
}

Usage:

final class Activity: Swift.Decodable {
    let id: Id<Activity>
    let name: String
}

extension API.Activity {
    static func get(activityId: Id<Activity>) -> Endpoint<Activity>
}

See Codable: Id Type and Single Value Container to learn how to add Codable conformance to Id type.

Authentication Scopes #

Another use case is the API Client where phantom types represent authentication scopes:

enum Scope { // enums used as namespaces
    enum Guest {}
    enum Customer {}
}

struct AuthorizedEndpoint<Authorization, Response> {
    let raw: Endpoint<Response>
}

struct AuthorizedClient<Authorization> {
    let raw: ClientProtocol
}

Extensions with generic where clauses are used to represent permissions in AuthorizedClient:

/// API client with a `Guest` authorization can only perform requests
/// with a lowest (`Guest`) authorization scope.
extension AuthorizedClient where Authorization == Scope.Guest {
    func request<Response>(_ endpoint: AuthorizedEndpoint<Scope.Guest, Response>) -> Single<Response> {
        return raw.request(endpoint.raw)
    }
}

/// API client with a `Customer` authorization can perform requests
/// with both `Guest` and `Customer` authorization scopes.
extension AuthorizedClient where Authorization == Scope.Customer {
    func request<Response>(_ endpoint: AuthorizedEndpoint<Scope.Guest, Response>) -> Single<Response> {
        return raw.request(endpoint.raw)
    }

    func request<Response>(_ endpoint: AuthorizedEndpoint<Scope.Customer, Response>) -> Single<Response> {
        return raw.request(endpoint.raw)
    }
}

For more info about the API client see API Client in Swift.

Layout Anchors #

The most recent and the most complex use case of phantom types for me was in Align, a small Auto Layout library which implements custom layout anchors. I think this is most interesting one, so I’m going to focus on it a bit more. Similar to NSLayoutAnchor each anchor in Align represents a layout attribute of a view. Unlike NSLayoutAnchor each kind of anchor has its own special set of methods which is part of Align’s fluent API.

First, let’s take a quick look at how NSLayoutAnchor represents different kinds of anchors. There is a base class NSLayoutAnchor and there are three subclasses:

You never use the NSLayoutAnchor class directly. Instead, use one of its subclasses, based on the type of constraint you wish to create.

  • Use NSLayoutXAxisAnchor to create horizontal constraints.
  • Use NSLayoutYAxisAnchor to create vertical constraints.
  • Use NSLayoutDimension to create constraints that affect the view’s height or width.

Their subclasses provide additional type checking, preventing you from creating invalid constraints. For example, you can’t create a constraint between left and top anchors.

Align needed more information about the kind of the attribute that the anchor was wrapping. Here’s an anchor “taxonomy” that I’ve come up with:

// Each anchor is parameterized with a type and an axis.
struct Anchor<Type, Axis> {}

// There are four types of anchors:
final class AnchorTypeDimension {}
final class AnchorTypeCenter: AnchorTypeAlignment {}
final class AnchorTypeEdge: AnchorTypeAlignment {}
final class AnchorTypeBaseline: AnchorTypeAlignment {}

/// Alignments include `center`, `edge` and `baselines` anchors.
protocol AnchorTypeAlignment {}

// And there are two phantom types to represent axis:
final class AnchorAxisHorizontal {}
final class AnchorAxisVertical {}

A combination of type and axis is used to represent different anchors:

var top: Anchor<AnchorTypeEdge, AnchorAxisVertical>
var left: Anchor<AnchorTypeEdge, AnchorAxisHorizontal>
var centerX: Anchor<AnchorTypeCenter, AnchorAxisHorizontal>
var firstBaseline: Anchor<AnchorTypeBaseline, AnchorAxisVertical>
var width: Anchor<AnchorTypeDimension, AnchorAxisHorizontal>

With all that extra information available at compile time (type and axis), I could add methods tailored for each specific type of anchor. Here are a few examples.

Each anchor which defines view’s alignment (center, edge and baseline) can be aligned with another alignment anchor, but only if the other anchor has the same axis (prevents users from creating invalid constraints!):

extension Anchor where Type: AnchorTypeAlignment {
    func align<Type: AnchorTypeAlignment>(with anchor: Anchor<Type, Axis>, offset: CGFloat = 0) -> NSLayoutConstraint
}

Each dimension anchor can match the other dimensions, no matter the axis.

extension Anchor where Type: AnchorTypeDimension {
    func match<Axis>(_ anchor: Anchor<AnchorTypeDimension, Axis>, offset: CGFloat = 0) -> NSLayoutConstraint

    // Or you can just set a constant size.
    func set(_ constant: CGFloat) -> NSLayoutConstraint
}

Edges can be pinned to a superview with insets (which is different from an offset!):

extension Anchor where Type: AnchorTypeEdge {
    func pinToSuperview(inset: CGFloat = 0) -> NSLayoutConstraint
}

With just a few phantom types I was able to add all that extra type information without having to subclass Anchor type (it is actually a simple struct). If I were to use the approach similar to NSLayoutAnchor it would lead to a class explosion (think AnchorEdgeVertical, AnchorCenterVertical, etc). More importantly, with generics I was able to target specific groups of anchors (e.g. Anchor<*, Vertical>, or Anchor<Dimension, *>)>).

Resources #

There are more examples of phantom types in Swift available online: