users.map(\.name)。最近のSwiftコードでよく見る1行です。バックスラッシュで始まる\.nameの正体を尋ねると、「mapの省略構文では?」と答えられることがあります。半分は正解です。これは独立した型であるKeyPathの値であり、mapが特別なのではなく、KeyPathを関数の代わりに使えるから成立する構文です。
中級シリーズ最終回の第8回です。KeyPathが「プロパティアクセスを値にする」とは何を意味するのか、3種類のKeyPath型の違い、そして実務で真価を発揮する場面を整理します。
KeyPathとは — プロパティへの道を値として扱う
クロージャの記事では、関数を値として扱う強力さを見ました。KeyPathはその考え方をプロパティアクセスに適用したものです。user.nameというアクセス操作からuserを切り離し、「.nameへの道」だけを残して値にしたものが\User.nameです。
let path = \User.name // KeyPath<User, String>
let user = User(name: "Kim", age: 30)
let name = user[keyPath: path] // "Kim"
型シグネチャが本質を示しています。KeyPath<User, String>は「Userから始まりStringに到達する経路」です。まだどのインスタンスにも結び付いていないため、変数に格納し、関数に渡し、配列にまとめられます。文字列キー(「name」)でプロパティにアクセスする動的言語との決定的な違いは型安全性です。\User.nmaeのようなタイプミスはコンパイルエラーになり、経路の出発型と到着型はすべてコンパイル時に検証されます。Objective-CのKVC(Key-Value Coding)文字列キーが担っていた処理を型システムに取り込んだものであり、第1回の安全性優先という思想がここでも繰り返されています。
経路は連結できます。\User.address.cityのようにネストしたプロパティをたどり、\User.name.countのように標準ライブラリのプロパティへ続けることもできます。appending(path:)で2つの経路を実行時に連結することも可能です。
関数の位置に立つKeyPath — map(.name)の仕組み
users.map(\.name)がコンパイルされるのは、Swift Evolution提案SE-0249のおかげです。(Root) -> Value関数を期待する場所にKeyPath<Root, Value>を渡すと、コンパイラーが{ $0[keyPath: path] }クロージャへ自動変換します。したがって、以下の2行は同じコードです。
let names = users.map { $0.name }
let names = users.map(\.name)
どちらがよいでしょうか。単純なプロパティ抽出ならKeyPathのほうがよい、というのが一般的な見解です。{ $0.name }は「クロージャを読む→$0の意味を把握する→プロパティを1つ取り出すのだと理解する」という3段階を踏みますが、\.nameは「nameを取り出す」こと自体が構文です。高階関数の記事で述べた意図の宣言を、さらに圧縮した形です。一方、変換ロジックが少しでも混ざるなら($0.name.uppercased() + "님")クロージャを使うべきです。KeyPathは抽出専用で、変換の道具ではありません。
この自動変換はmapだけでなく、関数を受け取るあらゆる場所で働きます。filter(\.isActive)、compactMap(\.thumbnail)、sorted(by:)で使うKeyPathComparator、contains(where:)まで。高階関数の便利なレシピがKeyPathと組み合わさることで、さらに短くなります。
3種類のKeyPath — 読み取り専用か、書き込みも可能か
KeyPathには階層があります。経路が許可するアクセスに応じて、コンパイラーは異なる型を生成します。
KeyPath<Root, Value> — 読み取り専用。 letプロパティや読み取り専用の計算プロパティへの経路です。
WritableKeyPath<Root, Value> — 読み書き可能。 varの格納プロパティ(またはsetterを持つ計算プロパティ)への経路で、値型のプロパティを経路経由で変更できます。
ReferenceWritableKeyPath<Root, Value> — 参照対象への書き込み。 クラスインスタンスのvarプロパティへの経路です。letで保持した参照でも中身は変更できるという値・参照の意味論が、KeyPath型にも反映されています。
この区別が実務で意味を持つのは、「経路を通して値を変更する」コードを書くときです。
func update<T, V>(_ items: inout [T], path: WritableKeyPath<T, V>, to value: V) {
for i in items.indices {
items[i][keyPath: path] = value
}
}
update(&cells, path: \.isSelected, to: false) // すべての選択を解除⟧
読み取り専用のKeyPathを渡すとコンパイルエラーになります。「この関数はそのプロパティを変更する」という契約がシグネチャに埋め込まれているのです。これは、失敗の可能性を関数シグネチャに刻むthrowsと同じ構文上の思想です。
実務での本当の出番 — 設定とロジックの分離
mapの省略構文はKeyPathの入り口にすぎません。本当の価値は、「どのプロパティを扱うか」をデータにする設計にあります。
ソート基準の外部化。 テーブルのソートUIを作るとき、列ごとにソート関数を書く代わりに、KeyPathComparatorの配列として宣言します。たとえば[KeyPathComparator(\.name), KeyPathComparator(\.date, order: .reverse)]です。ユーザーが選んだ列に応じてcomparatorだけを変更し、items.sorted(using:)に渡せば、ソートロジックは1行に固定され、基準だけがデータとして動きます。
フォームバインディング・検証テーブル。 「このフィールドはUserのこのプロパティに対応する」とKeyPathで宣言しておけば、フィールドの走査・検証・保存がテーブル駆動のコードになります。フィールドが増えてもロジックは増えません。
SwiftUIとObservationの基盤。 List(users, id: \.id)のidパラメーターはKeyPathであり、Observationフレームワークが「どのプロパティが読み取られたか」を追跡するのもKeyPathが基盤です。フレームワークが「あなたの型のどのプロパティか」を知る必要があるあらゆる場所で、KeyPathが標準的な共通通貨として使われます。
共通するパターンが見えてきます。ロジックは1つに固定し、どのプロパティに適用するかを値として注入する。ジェネリクスが型をパラメーターにしたように、KeyPathはプロパティの選択をパラメーターにします。Strategyパターンの超軽量版と考えてもよいでしょう。
注意点は乱用です。\.selfや多段階の経路が混ざったジェネリックAPIは、シグネチャが急激に難しくなります。Progressive Disclosureの原則に従い、複雑さが利用側へ漏れ始めたら、通常のクロージャや明示的な関数に戻すのが適切です。
まとめ
- KeyPathはプロパティへのアクセス経路を値にした型です。
\User.nameはKeyPath<User, String>であり、タイプミスや型の不一致はコンパイル時に検出されます。 map(\.name)はSE-0249による自動変換です。単純な抽出にはKeyPathを使い、変換が含まれるならクロージャを使います。- 階層は3つです。読み取り専用のKeyPath、値型に書き込むWritableKeyPath、参照対象に書き込むReferenceWritableKeyPath。変更を行う関数はWritableを要求し、その契約をシグネチャに刻みます。
- 真価はプロパティ選択のパラメーター化にあります。ソート基準、フォームバインディング、id指定のように「ロジックは1つで、対象プロパティだけを差し替える」場面の標準的な道具です。
これで中級シリーズ全8回が完結しました。次はSwift Concurrencyの連載となる上級シリーズです。第1回では、async/awaitがコールバックのどの問題をどのように解決したのか、そしてsuspensionという概念の正確な意味を扱います。

![[Swift中級 #8] Swift KeyPathまとめ、map(\.name)の仕組みのカバー画像](/assets/images/posts/ed14abe4-909b-4e97-9896-8e26955ce9e6/swift-keypath-1.jpg)