5 Things You Didn’t Know About KeyPaths
You’re more of a video kind of person? I’ve got you covered! Here’s a video with the same content than this article 🍿
KeyPaths are a pretty popular Swift syntax!
But do you think you know all the features of KeyPaths? 🤨
Here are 5 things you (probably) didn't know about KeyPaths!
Let’s start with #1: Whenever you use a method like .map { }
or .filter { }
, you can pass it a KeyPath
instead of a closure!
And the compiler will take care of automatically translating that KeyPath
to a closure 👌
Moving on to #2: Did you know there’s actually more than one type of KeyPath
?
The classic KeyPath
type is actually a reference to a read-only property.
When we take a KeyPath
to a writable property, we get a WritableKeyPath
:
And when working with a reference type, we even get a ReferenceWritableKeyPath
!
Most of the time, we don’t need to care about these different types, as the compiler will map a literal KeyPath
to the correct type.
However, if we write our own APIs then these differences become important, because we’ll need to use the correct type depending on what we want to do with the KeyPath
.
(And there are even a few additional types to deal with type erasure!)
Let’s move on to #3: We are all familiar with KeyPaths that reference properties, but did you know that a KeyPath
can also reference a subscript
?
Here, for instance, I’m taking a KeyPath
to the property name
of the second element in an array of Person
.
From there we can simply invoke that KeyPath
over the array just like we would do with a single value 👍
Now for #4, let’s talk about how we can use KeyPaths to write nice little DSLs, like this type-safe predicate syntax.
To make this syntax work in Swift, we actually only need to implement a single function!
We just need to implement an overload of the operator >
, that takes as its left hand side a KeyPath
and as its right hand side a constant, and then use the KeyPath
to compare the value of the property to that of the constant!
Finally for #5️⃣: Did you know we can use dynamic member lookup with a KeyPath
?
Consider the struct Order
I’ve just declared: I would like to be able to access the properties of the address
directly on the struct itself:
To do so, we start by annotating the struct
with @dynamicMemberLookup
.
Then, we implement a subscript
that takes as its argument a KeyPath to a property of an Address
and we invoke that KeyPath
on the property address
:
And that’s it, we can now directly access the properties of the address
!
Since our implementation of the subscript relies on a KeyPath
, this syntax is type-safe: meaning that if we try to call a property that doesn’t exist on an address
, it will result in a compilation error 👌
That’s all for this article, these were the 5 things you (probably) didn’t know about KeyPaths!
Here’s the code if you want to experiment with it:
import Foundation
// #1
struct Person {
let name: String
var age: Int
}
let people = [
Person(name: "John", age: 30),
Person(name: "Sean", age: 14),
Person(name: "William", age: 50),
]
people.map { $0.name }
people.map(\.name)
// #2
let readOnlyKeyPath = \Person.name // KeyPath<Person, String>
let readWriteKeyPath = \Person.age // WritableKeyPath<Person, Int>
// #3
let subscriptKeyPath = \[Person].[1].name
people[keyPath: subscriptKeyPath] // "Sean"
// #4
func > <Root, Value: Comparable>(
_ leftHandSide: KeyPath<Root, Value>,
_ rightHandSide: Value
) -> (Root) -> Bool {
return { $0[keyPath: leftHandSide] > rightHandSide }
}
people.filter(\.age > 18)
// #5
struct Address {
let city: String
let country: String
}
@dynamicMemberLookup
struct Order {
let customer: Person
let address: Address
/* ... */
subscript<T>(dynamicMember keyPath: KeyPath<Address, T>) -> T {
address[keyPath: keyPath]
}
}
let order = Order(
customer: Person(name: "Vincent", age: 32),
address: Address(city: "Lyon", country: "France")
)
order.city // equalivalent to `order.address.city`
order.country // equalivalent to `order.address.country`