A & B と書く交差型は、A であると同時に B でもある値を表します。オブジェクト型なら、値は両方のプロパティをすべて持つということです。プロパティを書き直さずに既存の型を組み合わせる方法です。
2つ目のオブジェクトは team を省いているので、コンパイルエラー TS2322 になり、メッセージの次の行には Property 'team' is missing ... but required in type 'Employee' と書かれています。Staff の値は、Person や Employee が期待される場所ならどこにでも渡せます。
オブジェクト型を組み合わせる
& は、型エイリアス、インターフェース、インラインのオブジェクト型をどう混ぜても使え、ジェネリクスの型パラメーターにも使えます。置き換えが難しいのはこの最後のケースです。受け取ったオブジェクトが何であれプロパティを追加する関数を、正確に表現できます。
呼び出し側は、渡したものの正確な型(title、words)に、追加された2つのプロパティを加えた型を受け取ります。インターフェースは型パラメーターを拡張できないので、interface ... extends では「T が何であれ、それにこれらを加えたもの」を表現できません。
衝突するプロパティは never になる
両側が同じプロパティを宣言していると、そのプロパティの型は2つの型の交差型になります。その2つの型に共通の値がなければ、プロパティは never になり、値を作ろうとするまでコンパイラーは何も言いません:
index.ts(7,22): error TS2322: Type 'string' is not assignable to type 'never'.
エラーは原因となった型ではなくオブジェクトを指すので、この種のバグは原因をたどるのに時間がかかります。エディターで r.id にカーソルを合わせると、型が never だとわかります。type Shape = { kind: "circle" } & { kind: "square" } のように衝突するプロパティがリテラルのタグなら、TypeScriptはさらに進んで交差型全体を never に縮約します。そうした値のプロパティを読むと、理由が説明されます: Property 'kind' does not exist on type 'never'. The intersection 'Shape' was reduced to 'never' because property 'kind' has conflicting types in some constituents.
interface ... extends なら、同じ衝突を宣言の位置でエラー TS2430 として捕まえます。これが2つの主な実用上の違いで、interface と type のページで並べて比較しています。
重なりがあればプロパティが狭まる
2つのプロパティの型に重なりがあれば、結果はその重なりになります。これはエラーではなく、便利な性質です:
後半は、& がユニオン型に何をするかを示しています。両側に共通するメンバーを残します。型を値の集合として考えると、これは予想どおりの結果です。A | B は2つの集合の和集合、A & B はその共通部分で、何もない共通部分が never です。
交差型とユニオン型
名前は集合論から来ていて、オブジェクトのプロパティに当てはめると逆に感じられます:
A | B(ユニオン型) | A & B(交差型) | |
|---|---|---|
| 値は | A または B | A かつ B |
| 許される値の集合 | 大きい | 小さい |
| 使えるプロパティ | 両方にあるものだけ | どちらかにあるものすべて |
string と number | string | number | never |
"a" | "b" と "b" | "c" | "a" | "b" | "c" | "b" |
オブジェクト型の交差型がより多くのプロパティを持つのは、まさに許す値が少ないから、つまりすべてを持つオブジェクトだけを許すからです。
交差型と extends
type C = A & B | interface C extends A, B | |
|---|---|---|
| 対象 | ユニオン型や型パラメーターを含むあらゆる型 | メンバーが静的にわかるオブジェクト型 |
| 衝突するプロパティ | 黙って never になる | 宣言の位置でエラー TS2430 または TS2320 |
| 結果 | 構成要素ごとにチェックされる交差型 | 関係がキャッシュされる、1つのフラットな名前付きの型 |
| 大きな組み合わせ | 型チェックが遅くなることがある | TypeScript wiki の Performance のページが推奨 |
いくつかのオブジェクト型のエイリアスを組み合わせるなら、& が慣用的で問題ありません。多くの部品から作る型や公開APIの型なら、extends のほうがエラーが早く出て、型チェックも軽くなります。
プリミティブとの交差型: ブランド化
プリミティブとオブジェクト型の交差型は never になりません。string & { readonly __brand: "UserId" } は、コンパイル時だけの目印を追加で持つ文字列です。本物の文字列はそのプロパティを持たず、それこそが狙いです。ブランドを意図的にアサーションするコードだけがそれを作れるので、UserId が期待される場所に普通の string や OrderId を渡せなくなります。この手法には専用のページがあります: ブランド型。
よくある質問
TypeScriptの交差型とは何ですか?
A & B と書く型で、その値は A と B の両方を同時に満たさなければなりません。オブジェクト型なら、値は A のすべてのプロパティと B のすべてのプロパティを持つということです。2つの型エイリアスを1つに組み合わせる一般的な方法です。
ユニオン型と交差型の違いは何ですか?
ユニオン型 A | B は「どちらか一方」を意味します。値は A か B で、絞り込むまでは共通するものしか使えません。交差型 A & B は「両方」を意味し、値は両方のすべてを持ちます。オブジェクト型では、ユニオン型はより多くの値を受け付け、交差型はより多くのプロパティを持ちます。
交差型が never になってしまうのはなぜですか?
両側を満たせる値がないからです。string & number は never で、{ id: string } & { id: number } では id が string & number になるので、プロパティは never になり、オブジェクトを作れません。2つのオブジェクト型が同じリテラルのタグを違う値で持つ場合(kind: "circle" と kind: "square")、交差型全体が never に縮約されます。
交差型と extends のどちらを使うべきですか?
どちらもオブジェクト型を組み合わせます。interface X extends A, B は衝突するプロパティを宣言の位置で報告し、大きなオブジェクト型を組み立てるときにTypeScriptチームが推奨しています。& は、extends では組み合わせられないユニオン型やジェネリクスの型パラメーターを含め、どんな型にも使えます。型エイリアスやジェネリックなヘルパーには & を、インターフェースを作るときは extends を使いましょう。
TypeScriptで2つのオブジェクト型をマージするには?
type Merged = A & B と書きます。実行時の値は、両方のオブジェクトをスプレッドします: const merged: A & B = { ...a, ...b }。A と B が違う型の同じプロパティを持っていると、そのプロパティの型は never になります。2つ目のオブジェクトのプロパティで1つ目を置き換えたいなら、Omit<A, keyof B> & B を使います。