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.