What are key paths in Swift, and where are they useful?
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] = 31The 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
| Type | Can read | Can write | Root |
|---|---|---|---|
KeyPath<Root, Value> | Yes | No | Any |
WritableKeyPath<Root, Value> | Yes | Yes | Value type (needs var) |
ReferenceWritableKeyPath<Root, Value> | Yes | Yes | Class (works through let) |
PartialKeyPath<Root> | Yes, as Any | No | Any |
AnyKeyPath | Yes, as Any? | No | Unknown |
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 propertiesYou 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 compilerSwiftUI'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 dismissBoth 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 beHashable. - Are key paths Sendable? In Swift 6, key path literals can be inferred
Sendablewhen they capture no non-Sendable values (like a subscript argument). Write the type asKeyPath<A, B> & Sendablewhen 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?#keyPathproduces aStringfor KVC/KVO. Swift key paths are typed values and work with structs.