This content originally appeared on Level Up Coding - Medium and was authored by Chris Nevin

A phantom type is a custom type that has one or more unused type parameters.
The simplest example would be this:
struct Phantom<Context, WrappedValue> {
let wrappedValue: WrappedValue
}
Why is this useful? It helps us ensure that developers can’t accidentally put an unexpected value in a field, for example putting a firstName in a lastName field could be restricted like so…
enum PhantomTypes {
enum FirstName {}
enum LastName {}
}
typealias FirstName = Phantom<PhantomTypes.FirstName, String>
typealias LastName = Phantom<PhantomTypes.LastName, String>
struct Client {
let firstName: FirstName
let lastName: LastName
}
Type-Safety
So now a developer can’t actually do firstName = lastName without the compiler throwing a type-mismatch error.
What if we had two entities with firstName properties? What’s to stop me setting a client’s firstName to a producer’s firstName? We can introduce nesting of the Phantom type on the first unused property.
enum PhantomTypes {
enum FirstName {}
enum LastName {}
}
protocol FirstNameHaving {}
extension FirstNameHaving {
typealias FirstName = Phantom<Phantom<Self, FirstName>, String>
}
struct Client: FirstNameHaving {
let firstName: FirstName
}
struct Producer: FirstNameHaving {
let firstName: FirstName
}
client.firstName = producer.firstName // type-mismatch
This is great! We can ensure our values will be set correctly at compile-time without having to write any tests of our value mappers.
However, these are still a bit bulky to use. If you try to create a Producer above you’d have to write:
Producer(firstName: FirstName("firstName"))
If only there was a way that we could hide away that we are using a custom type and just provide the guts to the firstName property...
ExpressibleBy…Literal
Thankfully, there is! Here’s an example of ExpressibleByStringLiteral.
extension Phantom: ExpressibleByStringLiteral where WrappedValue: ExpressibleByStringLiteral {
init(stringLiteral value: StringLiteralType) {
self = Self(WrappedValue(stringLiteral: value as! WrappedValue.StringLiteralType))
}
}
Now we could write the above initializer like:
Producer(firstName: "firstName")
Note: It’s still a FirstName and you can still pass in a FirstName object.
There’s still one downside, if we wanted to check if the firstName was not empty (for example) we’d have to write something like:
producer.firstName.wrappedValue.isEmpty
It would be great if we could get rid of that wrappedValue so our use of phantom-types are transparent…
@dynamicMemberLookup
Syntactic sugar completes the final piece of the puzzle, here’s how we could add it to our Phantom struct:
@dynamicMemberLookup
struct Phantom<Context, WrappedValue> {
var wrappedValue: WrappedValue
init(_ wrappedValue: WrappedValue) {
self.wrappedValue = wrappedValue
}
subscript<T>(dynamicMember member: KeyPath<WrappedValue, T>) -> T {
get { return wrappedValue[keyPath: member] }
}
}
This allows us to access the properties of the WrappedValue directly from the Phantom type, for example:
producer.firstName.isEmpty
@propertyWrapper
We can take this one step further by introducing @propertyWrappers which take a WrappedType.
struct Producer: FirstNameHaving {
@Truncated(maxLength: 10)
var firstName: FirstName
}
producer.firstName = "Christopher"
print(producer.firstName) // "Christophe"
Thanks for reading!
I’ve created a Swift Package which contains the above plus many more protocol conformances, check it out… https://github.com/cjnevin/PhantomTypes
Expressible Dynamic Phantom Types was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.
This content originally appeared on Level Up Coding - Medium and was authored by Chris Nevin

Chris Nevin | Sciencx (2022-10-22T12:06:16+00:00) Expressible Dynamic Phantom Types. Retrieved from https://www.scien.cx/2022/10/22/expressible-dynamic-phantom-types/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.