Swift Subscripts and Key Paths

Design clear indexed access, return absence deliberately, and use typed key paths to select properties without string-based field names.

Lesson 29 of 43Intermediate9 min read

Bracket syntax often means accessing an element, but Swift lets your own types define subscripts too. A subscript can represent a lookup by a name, coordinate or other meaningful index. Key paths solve another access problem: they describe a typed route to a property so generic code can select it without using an unvalidated string name.

Start with the arrays lesson and the generics lesson. This reference combines access syntax with explicit absence and mutation policies. Short syntax is helpful only when its behavior remains predictable to somebody reading the call site.

A subscript is an access contract

A custom subscript declares parameter types and a result type. Its getter computes the result, and an optional setter supports assignment. It does not need to index a physical array; a dictionary-backed table can look up a named value, while a matrix can use two coordinates.

Choose what missing access means. Dictionary-style lookup often returns an optional. Array-style indexing has a precondition that the index is valid and can trap if callers violate it. A custom type should document its rule rather than imitate brackets while silently changing their expected failure behavior.

Keep hidden work proportional to what callers can reasonably expect. A bracket lookup that starts a remote request or deletes records is surprising. A named method can communicate expensive or side-effecting behavior better. Subscripts cannot use ordinary async throwing lookup syntax as a substitute for a well-designed operation API.

Typed property selection with key paths

A key-path literal such as backslash Product.name describes a property route from a root type to a value type. A KeyPath<Product, String> can read a String property from a Product. Access uses the keyPath subscript, and the compiler checks both the root and value types.

This is different from asking for a field named "name" through a runtime string. A misspelled Swift property in a literal is a compilation problem, and a generic function can constrain the exact property result it needs. Key paths can also include supported chains of properties and optional operations according to their type.

WritableKeyPath supports mutation where the path and root permit it. ReferenceWritableKeyPath represents writable access through reference semantics. A read-only KeyPath cannot be used to assign merely because its underlying property was originally var; the abstraction's declared capability matters. Keep the narrowest capability your function actually requires.

Mutation follows the root's semantics

Writing through a WritableKeyPath to a struct changes that value, so a generic mutation function needs an inout root. Changing a property of a referenced class instance follows reference semantics, with its own ownership and concurrency concerns. A key path does not make shared class mutation safe or bypass access control.

Do not assume a key path is always a stored memory offset. It can describe computed access, and evaluating that access can run getter logic. Avoid repeatedly evaluating an expensive computed property in a loop when one explicit cached local result would be clearer.

Key paths are useful for reusable selectors, sorting configuration and field extraction. Closures remain more flexible for arbitrary transformations. Choose a closure when the operation computes a new meaning from several properties; choose a key path when it selects an existing property route. Neither tool needs to replace the other everywhere.

A writable lookup also needs an insertion or removal policy. For example, assigning nil through a dictionary-like optional setter might remove an entry, while an absent nonoptional value could require an explicit creation method. Document that behavior beside the getter's missing-key rule. Brackets alone cannot tell a caller whether assignment replaces, inserts, rejects or deletes, so surprising mutation is a reason to prefer named operations.

Official references

The Swift subscript guide explains custom accessors. The expressions reference documents key-path expressions; its official source includes the typed syntax. Review missing values, access cost, write capability and root ownership before publishing a custom access interface.

Swift Subscripts and Key Paths Examples

Each example shows the code, the output it produces and what each part does.

Example 1

A read-only named score table

The optional result makes missing names visible. The fallback negative one is only demonstration output; callers should choose a meaningful missing-state representation for their interface. The table stores an immutable dictionary value and exposes no setter, so clients cannot assign a score through this subscript.

Swift
struct ScoreTable {
    private let scores: [String: Int]
    init(scores: [String: Int]) { self.scores = scores }
    subscript(name: String) -> Int? { scores[name] }
}

let table = ScoreTable(scores: ["Mina": 7, "Ali": 4])
print(table["Mina"] ?? -1)
print(table["Unknown"] == nil)
Output
7
true

Swift Subscripts and Key Paths in Real Projects

Worked exercise: Extract product names generically

Write a generic function that extracts a selected property from every element. Its caller supplies a typed key path rather than a field-name string. Demonstrate both a String property and an Int property using the same implementation.

Swift
struct Product {
    var name: String
    var stock: Int
}

func values<Root, Value>(from roots: [Root], at path: KeyPath<Root, Value>) -> [Value] {
    roots.map { $0[keyPath: path] }
}

let products = [Product(name: "Pen", stock: 3), Product(name: "Book", stock: 2)]
print(values(from: products, at: \Product.name))
print(values(from: products, at: \Product.stock))

var product = products[0]
let stockPath: WritableKeyPath<Product, Int> = \Product.stock
product[keyPath: stockPath] = 8
print(product.stock)
print(products[0].stock)
Output
["Pen", "Book"]
[3, 2]
8
3

Mutating the local struct copy does not change the original array element. The extractor only needs read capability, so its parameter uses KeyPath rather than WritableKeyPath. Add tests for empty input and computed properties. If the array holds class instances, copying the array would not create independent copies of those objects.

Swift Subscripts and Key Paths FAQ

Should a custom subscript always return an optional?

No. The result should match the abstraction's contract. Dictionary-like lookup naturally permits a missing key, so an optional communicates absence. A fixed-domain matrix may require valid coordinates as a precondition, while another matrix API might offer an explicitly safe optional lookup. Document the policy and keep it consistent. Do not return a fabricated value that callers cannot distinguish from a real stored result merely to avoid handling absence.

Are key paths just property names stored as strings?

No. A key path is a typed property route with a root type and result type, and supported writable variants express additional capability. The compiler checks a literal against real declarations. A runtime string can contain any text and needs a separate interpretation mechanism. Use key paths for type-safe in-process selection; external field names in JSON or a query protocol still require their own mapping and validation policy.

Why does a generic setter need inout for a struct root?

Structs have value semantics, so mutation must affect the caller's particular value rather than an unrelated local copy. Inout grants the function a controlled mutation relationship for that call. A WritableKeyPath describes how to reach writable state but does not by itself change parameter passing semantics. Keep the root mutable and make the mutation explicit. For reference roots, consider who else observes the same object and which actor or thread owns it.

Should I replace every closure with a key path?

Only when the closure simply selects a suitable property and the API supports that form. Closures can compute derived values, combine fields, validate conditions or perform more complex transformations. A key path communicates a selected route and can be useful as reusable configuration. Prefer whichever clearly expresses the operation's meaning. Conciseness alone is not enough if replacing a closure hides computation or makes the generic types harder to understand.

Swift Subscripts and Key Paths Exercises

Answer each one and check it instantly. If you get stuck, open the hint before the answer.

Multiple choicemedium

A generic function only extracts properties. Which parameter capability is sufficient?

Swift Subscripts and Key Paths Quiz

2 questions to check what you learned.

  1. What does KeyPath<Product, String> permit?

    • Reading a String result through a supported Product property route.
    • Accessing arbitrary fields chosen by unchecked strings.
    • Assigning any property of every type.
  2. Why does the score table return Int? from its subscript?

    • Bracket syntax requires all Swift results to be optional.
    • A requested name might not exist.
    • Every stored score is necessarily invalid.