Swift's functional collection methods, such as map(_:), compactMap(_:), and filter(_:), accept a closure that describes how to process each element. Many of these closures do only one thing: they read a property.
let usernames = users.map { $0.username }The closure syntax here adds braces and an anonymous $0 argument that say nothing about what the code does. Starting in Swift 5.2, you can pass a key path expression anywhere a function of the form (Root) -> Value is expected. The compiler converts the key path into the equivalent function for you.
let usernames = users.map(\.username)The result is identical. The intent, map each user to its username, is now the only thing on the line.
This style is often called point-free, because the code describes the transformation without naming the individual element (the "point") it operates on.
The examples in this article use the following types.
struct Address {
let city: String
let postalCode: String?
}
struct User {
let id: UUID
let username: String
let email: String?
let age: Int
let isActive: Bool
let isAdmin: Bool
let department: String
let address: Address
}Use a key path with map(_:) to extract a single property from every element in a collection.
let ids = users.map(\.id) // [UUID]
let ages = users.map(\.age) // [Int]
let departments = users.map(\.department)Key paths can also traverse nested properties. Chain property names with dots, just as you would in an ordinary expression.
let cities = users.map(\.address.city)When a property is optional, compactMap(_:) returns only the non-nil values. A key path to an optional property works exactly as a closure would.
// Before
let emails = users.compactMap { $0.email }
// After
let emails = users.compactMap(\.email) // [String]Key paths support optional chaining, so you can reach through optional values along the way.
let postalCodes = users.compactMap(\.address.postalCode)To remove nil values from a collection of optionals, use the identity key path, \.self.
let scores: [Int?] = [98, nil, 72, nil, 85]
let recordedScores = scores.compactMap(\.self) // [98, 72, 85]Any method that takes a predicate of the form (Element) -> Bool accepts a key path to a Bool property.
let activeUsers = users.filter(\.isActive)
let firstAdmin = users.first(where: \.isAdmin)
let hasAdmin = users.contains(where: \.isAdmin)
let everyoneActive = users.allSatisfy(\.isActive)Each line now reads almost like a sentence: filter users that are active, find the first user who is an admin.
Initializers that take a transformation closure also accept key paths. Use Dictionary(grouping:by:) to bucket elements by a shared property.
let usersByDepartment = Dictionary(grouping: users, by: \.department)
// [String: [User]]Combine key paths with other initializers to build lookups concisely.
let uniqueCities = Set(users.map(\.address.city))Some standard library methods, such as sorted(by:) and max(by:), expect a closure that takes two elements. A key path can't be converted into that shape directly. You can bridge the gap with a small extension.
extension Sequence {
func sorted<Value: Comparable>(
by keyPath: KeyPath<Element, Value>,
using areInIncreasingOrder: (Value, Value) -> Bool = (<)
) -> [Element] {
sorted {
areInIncreasingOrder($0[keyPath: keyPath], $1[keyPath: keyPath])
}
}
}The call site then matches the style of the other examples.
let youngestFirst = users.sorted(by: \.age)
let oldestFirst = users.sorted(by: \.age, using: >)Similarly, you can define an operator to negate a Boolean key path, which is useful because filter(\.isActive) has no built-in inverse.
prefix func ! <Root>(keyPath: KeyPath<Root, Bool>) -> (Root) -> Bool {
{ !$0[keyPath: keyPath] }
}
let inactiveUsers = users.filter(!\.isActive)Tip: Keep extensions like these small and few. Each one is another piece of vocabulary your team must learn, so add them only where they make repeated code clearer.
Key path expressions are a focused tool. Prefer a closure in the following situations.
The transformation does more than read a property. Key paths can't call methods, perform arithmetic, or combine values.
// Requires a closure
let lowercased = users.map { $0.username.lowercased() }
let summaries = users.map { "\($0.username) (\($0.age))" }The key path is stored in a variable. The automatic conversion applies only to key path literals written directly at the call site. A key path held in a constant or variable must be applied explicitly.
let nameKeyPath = \User.username
// users.map(nameKeyPath) // Error: cannot convert value of type 'KeyPath<User, String>'
let names = users.map { $0[keyPath: nameKeyPath] }The type can't be inferred. A leading-dot key path such as \.username relies on the surrounding context to determine its root type. In rare cases where the compiler can't infer it, write the root type explicitly, as in \User.username.
Important: Key path–to–function conversion requires Swift 5.2 or later. If your project supports an earlier toolchain, continue using closures.
Key paths don't replace closures; they replace a specific, very common kind of closure. Use these guidelines to decide which form to write:
| If the closure… | Prefer |
|---|---|
| Reads one property, possibly nested | Key path |
Tests a single Bool property |
Key path |
| Unwraps an optional property | Key path with compactMap(_:) |
| Calls a method or computes a new value | Closure |
| Compares two elements | Closure, or a key path–based extension |
When you apply them consistently, key paths let readers scan a chain of collection operations and see what each step extracts, without wading through the syntax of how.
let activeAdminEmails = users
.filter(\.isActive)
.filter(\.isAdmin)
.compactMap(\.email)Key path expressions give you a concise way to describe the most common collection transformations in Swift. When a closure does nothing more than read a property, replacing it with a key path removes the braces and the anonymous $0 argument, leaving only the property you care about.
Keep these points in mind as you adopt the pattern:
map(_:), compactMap(_:), filter(_:), first(where:), contains(where:), and allSatisfy(_:) whenever the closure would only read a property.\.address.city and \.address.postalCode, to reach deeper into your models without extra syntax.sorted(by:), only where they make repeated code clearer.Applied consistently, key paths make a chain of collection operations read like a description of your data rather than a set of instructions for processing it. Start with the simplest cases, such as users.map(\.username), and let the pattern spread naturally through your codebase.
Thank you for reading. If you have any questions feel free to follow me on X and send me a DM. If this article helped you, Buy me a coffee.