iosNudge

What are key paths in Swift, and where are they useful?

MediumSwift 2 min read

TL;DR

A key path is a reference to a property, not its value. You write it as \Type.property. You can pass it around and use it later with value[keyPath: path]. It is fully type-checked. You see it in map(\.name), sorted(using:), SwiftUI ForEach(id: \.self), and @dynamicMemberLookup.

The basics

struct User {
    var name: String
    var age: Int
    var isAdult: Bool { age >= 18 }
}

let nameKey = \User.name            // KeyPath<User, String>
var user = User(name: "Ana", age: 30)

print(user[keyPath: nameKey])       // "Ana"

let ageKey = \User.age              // WritableKeyPath<User, Int>
user[keyPath: ageKey] = 31

The type of the key path includes the root type (User) and the value type (String).

Key paths as functions

A key path can be used where a function (Root) -> Value is expected (Swift 5.2+).

let users = [User(name: "Ana", age: 30), User(name: "Ben", age: 25)]

let names = users.map(\.name)              // ["Ana", "Ben"]
let adults = users.filter(\.isAdult)       // [Ana, Ben]

This is short and readable. Interviewers like this one.

Key path types

TypeCan readCan writeRoot
KeyPath<Root, Value>YesNoAny
WritableKeyPath<Root, Value>YesYesValue type (needs var)
ReferenceWritableKeyPath<Root, Value>YesYesClass (works through let)
PartialKeyPath<Root>Yes, as AnyNoAny
AnyKeyPathYes, as Any?NoUnknown

A let property or a computed property with only a getter gives a plain KeyPath.

Generic sorting by any property

extension Sequence {
    func sorted<T: Comparable>(by keyPath: KeyPath<Element, T>) -> [Element] {
        sorted { $0[keyPath: keyPath] < $1[keyPath: keyPath] }
    }
}

users.sorted(by: \.age)
users.sorted(by: \.name)

Foundation also has KeyPathComparator:

users.sorted(using: KeyPathComparator(\.age, order: .reverse))

Chaining and optionals

struct Address { var city: String }
struct Person { var address: Address? }

let cityPath = \Person.address?.city      // KeyPath<Person, String?>
let lengthPath = \User.name.count         // goes through nested properties

You can also append key paths: \Person.address appending \Address.city.

Setting values generically

func update<Root, Value>(
    _ root: inout Root,
    _ path: WritableKeyPath<Root, Value>,
    to value: Value
) {
    root[keyPath: path] = value
}

update(&user, \.name, to: "Anna")

This pattern is useful for forms, settings screens and reducers.

Dynamic member lookup

@dynamicMemberLookup with a key path subscript forwards properties from a wrapped value, with full type safety.

@dynamicMemberLookup
struct Tracked<Value> {
    private(set) var value: Value

    subscript<T>(dynamicMember path: KeyPath<Value, T>) -> T {
        value[keyPath: path]
    }
}

let tracked = Tracked(value: user)
print(tracked.name)   // forwards to user.name, checked by the compiler

SwiftUI's Binding uses this. $model.name gives a Binding<String> from a Binding<Model>.

In SwiftUI

ForEach(names, id: \.self) { Text($0) }
@Environment(\.dismiss) private var dismiss

Both use key paths. \.dismiss is a key path into EnvironmentValues.

Common follow-up questions

  • Are key paths slower than direct access? A little. Direct property access is faster. In hot loops prefer closures or direct access.
  • Can a key path use a subscript? Yes. \[String].[0] points to the first element of an array. Subscript arguments must be Hashable.
  • Are key paths Sendable? In Swift 6, key path literals can be inferred Sendable when they capture no non-Sendable values (like a subscript argument). Write the type as KeyPath<A, B> & Sendable when you need to send one across tasks.
  • What is \.self? The identity key path. It returns the whole value. ForEach(..., id: \.self) uses the value itself as its id.
  • What is the difference from Objective-C #keyPath? #keyPath produces a String for KVC/KVO. Swift key paths are typed values and work with structs.