Key Paths and Dynamic Member Lookup
|
This section documents the Swift 6 language mode as shipped by Swift 6.3, as published in The Swift Programming Language at docs.swift.org, which is the reference these pages are written and verified against. 6.4-beta-only features are always flagged as such — never presented as baseline. This content was generated with the assistance of AI and should be verified against docs.swift.org before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
A key path is a first-class value that refers to a property without reading it — \Type.property rather
than instance.property — so a property reference can itself be stored, passed around, and applied to more
than one instance later. @dynamicMemberLookup builds on the same .member syntax to route arbitrary member
access through custom logic instead of a compile-time-known stored property.
Key-Path Expressions
struct Person {
var name: String
var age: Int
}
let ada = Person(name: "Ada", age: 36)
let nameKeyPath = \Person.name // KeyPath<Person, String> -- refers to the property, doesn't read it
ada[keyPath: nameKeyPath] // "Ada" -- reading through the key path
A key-path expression \Type.path.to.property evaluates to a KeyPath<Root, Value> value — Root is the type
the path starts from, Value the type at the end of it — and works through nested properties and optional
chaining the same way ordinary dot syntax does (\Person.address?.city).
The Key Path Hierarchy
class Counter {
var count = 0
}
let readOnlyPath: KeyPath<Person, String> = \Person.name // read-only
let writablePath: WritableKeyPath<Person, Int> = \Person.age // read-write, value type
let referenceWritablePath: ReferenceWritableKeyPath<Counter, Int> = \Counter.count // read-write through a reference
var mutableAda = ada
mutableAda[keyPath: writablePath] = 37 // writes through a WritableKeyPath
let counter = Counter()
counter[keyPath: referenceWritablePath] = 5 // ReferenceWritableKeyPath can write even through a `let` counter
let partial: PartialKeyPath<Person> = \Person.age // Root known, Value erased
let anyPath: AnyKeyPath = \Person.name // both Root and Value erased
KeyPath<Root, Value> is the read-only base; WritableKeyPath<Root, Value> adds writing through a var (or an
inout value-type root); ReferenceWritableKeyPath<Root, Value> further specializes this for a reference-type
root, letting the write happen even when the variable holding the reference is itself a let — mirroring the
value-vs-reference distinction from
Structures and Classes. PartialKeyPath<Root>
erases the value type while keeping the root type known (useful for heterogeneous collections of key paths into
the same root), and AnyKeyPath erases both, at the cost of needing as? casts to use the value on the other
end.
Key Paths as Functions
let people = [Person(name: "Ada", age: 36), Person(name: "Grace", age: 85)]
let names = people.map(\.name) // KeyPath<Person, String> used directly as a (Person) -> String
let sorted = people.sorted(using: KeyPathComparator(\.age))
let fullPath = (\Person.name).appending(path: \String.count) // KeyPath<Person, Int> -- composed key path
people.map(fullPath) // [2, 5] -- length of each name
The standard library overloads map, sorted(using:) and similar higher-order algorithms to accept a
KeyPath<Root, Value> directly wherever a (Root) → Value closure is expected — \.name reads exactly like
the equivalent { $0.name } closure but as data rather than executable code, which is what makes it usable with
KeyPathComparator and other APIs that need to inspect which property was chosen, not just call a function.
appending(path:) composes two key paths end to end, producing a single key path from the first’s root to the
second’s value — the key-path equivalent of chaining .a.b.
@dynamicMemberLookup
@dynamicMemberLookup
struct JSON {
private var storage: [String: Any]
subscript(dynamicMember member: String) -> Any? {
storage[member]
}
}
let json = JSON(storage: ["name": "Ada", "age": 36])
json.name // Any? -- rewritten by the compiler to json[dynamicMember: "name"]
@dynamicMemberLookup
struct Wrapper<Value> {
var value: Value
subscript<T>(dynamicMember keyPath: KeyPath<Value, T>) -> T {
value[keyPath: keyPath]
}
}
let wrapped = Wrapper(value: ada)
wrapped.name // rewritten to wrapped[dynamicMember: \Value.name] -- fully type-checked, no `Any` involved
@dynamicMemberLookup tells the compiler to rewrite any .member access the type doesn’t already have a
stored/computed property for into a call to a subscript(dynamicMember:) it must implement. A String-keyed
overload (first example) trades away compile-time member checking for maximum flexibility — useful for wrapping
loosely-typed data like decoded JSON — while a KeyPath-keyed overload (second example) keeps full static type
checking: the compiler still verifies \Value.name is a real, correctly-typed key path on Value, it just
routes the access through the wrapper rather than exposing `Value’s properties directly. A type may provide both
overloads simultaneously, and the compiler picks whichever one type-checks at each call site.
See Also
-
Properties — the stored and computed properties key paths refer to.
-
Generics — the generic subscript
subscript<T>(dynamicMember:)pattern used for a type-safe dynamic member lookup. -
Functional Programming —
map(\.property)in the broader context of higher-order functions. -
Methods and Subscripts — subscripts in general, including the
dynamicMember:label that gives this feature its name.
References
TSPL: Expressions → Key-Path Expression; Attributes → dynamicMemberLookup.