TypeScriptは構造型(structural typing)または「アヒル型付け」を使用しています。型の互換性においては、名前や起源ではなく、構造(シグネチャ)が重要です。
例:
interface Point2D { x: number; y: number; } interface Coord2D { x: number; y: number; } // これらの型は互換性があります。Point2DとCoord2Dは、構造が同じだからです。 const foo: Point2D = { x: 1, y: 2 }; const bar: Coord2D = foo; // OK!
名義型(nominal typing):型の互換性においては「名前」や「ファクトリー」が重要で、構造は重要ではありません。
TypeScriptでは名義型は標準ではサポートされていませんが、branded typesを使ってエミュレートすることができます:
type USD = number & { readonly __brand: unique symbol } type EUR = number & { readonly __brand: unique symbol } let priceUSD: USD; let priceEUR: EUR; // priceUSD = priceEUR; // エラー!異なるブランドです。
なぜこれを適用するのか? 例えば、構造は同じだが意味が異なる型を区別するため—通貨、userID/tokenID、物理量など。
質問: 次のコードは、AddressとUserIdが論理的に異なる型であるにも関わらず、なぜエラーなくコンパイルされるのですか?
interface Address { value: string; } interface UserId { value: string; } let id: UserId = { value: "test" }; let addr: Address = id; // OK
回答: TypeScriptでは構造が重要であり、型の名前は重要ではありません。両方の型は「value: stringを持つオブジェクト」です。
物語
プロジェクト: USD/EURでの計算を伴う金融システム。金額はnumberを介して渡された。一度、合計の際に通貨を間違えた — 構造型によりTypeScriptはこれを検出できなかった。後にbranded typesを導入し、コンパイル時にそのようなエラーを排除した。
物語
プロジェクト: REST APIの開発では、異なるエンティティ(userId、groupId)のIDを表すオブジェクトを使用しました。どちらもvalue: stringというフィールドを持っていました。誤ってuserIdがgroupIdの代わりに使用され、サーバーのビジネスロジックでのみエラーが検出されました。
物語
プロジェクト: DSLのパーサーライブラリでは、同じ構造(type value = { kind: 'num'|'str', value: number|string })を使用していました。同型の構造が異なるコードの部分で混在し、論理エラーを引き起こしました。区別のために人工的なbrandフィールドを追加しました。