TypeScript 複雑な型と互換性
オブジェクト型の、型Aと型Bが存在するとき、型Bが型Aのサブタイプとして見なされるのは、次の両方の条件が満たされる場合。
型Aに存在するすべてのプロパティが型Bに存在する
型Bのプロパティの型が、対応する型Aのプロパティの型のサブタイプである
これを型の関係の記号で表現すると、「Bの各プロパティ型 <: 各Aのプロパティの型」となる。この型の比較は再帰的に実行され、プロパティがさらにオブジェクト型を持つ場合、そのオブジェクト型に対しても同じサブタイプが適用される。
オブジェクト型の構造的部分的片付け
interface Person {
name: string
age: number
}
let person: Person
// ケース1
const john = {
name: 'John',
age: 30,
gender: 'male', // Personにはないプロパティが存在する
}
// OK 変数johnの各プロパティ型 <: Personの各プロパティ
person = john
// ケース2
const jane = {
name: 'Jane',
age: '25', // string型はPersonのageの型のサブタイプではない
}
// NG
// person = jane
// 型 '{ name: string; age: string; }' を型 'Person' に割り当てることはできません。
// プロパティ 'age' の型に互換性がありません。
// 型 'string' を型 'number' に割り当てることはできません
// ケース3
const alice = {
name: 'Alice',
// Personに存在するプロパティageが欠如している
}
// NG
// person = alice
// プロパティ 'age' は型 '{ name: string; }' にありませんが、型 'Person' では必須です。 上記の例の、ケース1では、変数johnにはPersonインターフェイスに定義されているプロパティがすべて含まれており、加えてPersonには定義されていない追加のプロパティがある。この場合、「johnの各プロパティの型 <: Personの各プロパティの型」の条件を満たしているため、変数personにjohnを代入することが可能。
ケース2では、変数janeはPersonが持つプロパティをすべて保持しているが、ageプロパティの型(string型)はPersonのageの型(number型)のサブタイプではないので代入できない。
ケース3では、変数aliceはPersonの必須プロパティageを持っていないため、Personにaliceを代入することはできない。
この構造的部分型付けは、関数の引数としてオブジェクト型の値を渡す際にも同じように適用される。
関数のパラメータがオブジェクト型の場合の構造的部分型付け
interface Person {
name: string
age: number
}
const person = {
name: 'John',
age: 30,
gender: 'male',
}
function introduce(person: Person) {
console.log(`Hello, I'm ${person.name}`)
}
introduce(person) // OK 上記の例では、Person型のパラメータを持つ関数introduceに、変数johnを渡して実行している。この場合も「johnの各プロパティ <: Personの各プロパティ」の関係を満たすので問題なく実行可能。
構造的型付けとは対照的に、型の互換性を型に付与された"名前"に基づいて判断する名前的型付け(nominal typing)というシステムも存在する。この型システムはC++やJavaなどの言語で見られ、2つの型が同じ構造を持っていても、名前が異なる場合には異なる型として扱われる。
JavaScriptでは無名の関数式やオブジェクトリテラルが広く使われているため、名前的型付よりも 構造的型付けによる型の互換性の判断がより適している。