TL;DR
- TypeScript は構造的型付けで、JavaScript のダックタイピングをモデリングしている。
- 構造的型付けでは、値は型宣言に書かれていないプロパティを持っている可能性がある。
内容
構造で型を判断する
JavaScript はダックタイピングを前提とした言語であり、TypeScript は構造的型付けでこれをモデリングしている。構造的型付けでは、型の名前ではなく、型の「構造」が一致するかで代入できるかを判断する。次の例では、NamedVector は Vector2D の x と y を持っているので、calcLen に渡しても型エラーにならない。
interface Vector2D {
x: number
y: number
}
interface NamedVector {
name: string
x: number
y: number
}
const calcLen = (v: Vector2D) => Math.sqrt(v.x ** 2 + v.y ** 2)
const v: NamedVector = { x: 3, y: 4, name: "Pythagoras" }
calcLen(v) // 型エラーにならない型は「開いている」
構造的型付けでは、関数の引数などの値は、型宣言に書かれていないプロパティを持っている可能性がある。関数を実装するときは、宣言したプロパティだけを持つ型(閉じた型、シールされた型)が渡されると考えがちだが、TypeScript ではそのような型を表現できない。そのため、特にオブジェクトのプロパティを反復処理するときに、予期しない挙動になりやすい(項目60)。
クラスも例外ではない。次の例では、コンストラクタの検証を通っていない値でも、構造が同じなので SmallNumContainer 型に代入できてしまう。
class SmallNumContainer {
num: number
constructor(num: number) {
if (num < 0 || num >= 10) {
throw new Error(`You gave me ${num} but I want something 0-9`)
}
this.num = num
}
}
const a = new SmallNumContainer(5)
const b: SmallNumContainer = { num: 2024 } // 型エラーにならない構造的型付けの利点
構造的型付けには利点もある。1 つ目は、テストでモックを作りやすいことである。たとえば次の getAuthors には、本物のデータベースの代わりに、runQuery を持つ任意のオブジェクトを渡せる。
interface DB {
runQuery: (sql: string) => any[];
}
const getAuthors = (database: DB): Author[] => { ... };2 つ目は、ライブラリ間の依存関係を切り離せることである(項目70)。