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`
Previous
Previous

Discover Property Wrappers in Swift

Next
Next

Discover Actors in Swift