TypeScript 関数型
戻り値の型
パラメータの型が互換性を有する2つの関数AとBにおいて、もし「Bの戻り値の型 <: Aの戻り値の型」の関係が成り立つならば、関数Bは関数Aのサブタイプとして見なされる。
戻り値の型と関数型の互換性
let fn1 = () => ({ name: 'John' })
// 関数型: () => { name: string }
const fn2 = () => ({ name: 'John', age: 30 })
// 関数型: () => { name: string, age: number }
// OK
fn1 = fn2
// fn2の戻り値 <: fn1の戻り値の型 であるため、fn1にfn2が代入可能
// NG
// fn2 = fn1
// 型 '() => { name: string; }' を型 '() => { name: string; age: number; }' に割り当てることはできません。
// プロパティ 'age' は型 '{ name: string; }' にありませんが、型 '{ name: string; age: number; }'パラメータの型
戻り値の型が互換性を持つ2つの関数AとBを考えた場合、以下の条件を満たすときに、関数Bは関数Aのサブタイプとなる。
- 対応する各パラメータにおいて、「Aのパラメータの型 <: Bのパラメータの型」である。
- Bのパラメータ数 <= Aのパラメータ数
1つ目の条件には注意が必要。なぜなら、オブジェクト型の互換性で見た関係とは逆なため。オブジェクトの場合は、オブジェクトの「各プロパティの型がサブタイプ」であれば、そのオブジェクト自体もサブタイプになる。
一方、関数型の場合は、関数Bのパラメータの型が、関数Aのパラメータの型のスーパータイプであれば、関数Bは関数Aのサブタイプとなる。
interface Person {
name: string
age: number
}
// インターフェイスの拡張によって、自動的にStudentはPersonのサブタイプになる
interface Student extends Person {
club: string
}
// Studentインターフェイスの構造
// {
// name: string
// age: number
// club: string
// } 上記の例では、Personインターフェイスを拡張してStudentインターフェイスを定義している。この拡張によって、Studentインターフェイスは、Personインターフェイスが持つすべてのプロパティの他に、追加でclubプロパティを持つことになり、「Student <: Person」というサブタイプ関係が自然に成立。
パラメータの型と関数型の互換性
interface Person {
name: string
age: number
}
// インターフェイスの拡張によって、自動的にStudentはPersonのサブタイプになる
interface Student extends Person {
club: string
}
// Studentインターフェイスの構造
// {
// name: string
// age: number
// club: string
// }
let fn3 = (person: Person) => {
console.log(`That person's name is ${person.name} (${person.age})`)
}
// fn3 は関数型: (person: Person) => void
let fn4 = (student: Student) => {
console.log(
`That student's name is ${student.name} (${student.age}) and enjoys ${student.club} club`,
)
}
// fn4 は関数型: (student: Student) => void
// NG
// パラメータの型に注目すると、Student型 <: Person型 なので条件を満たさない
// fn3 = fn4
// 型 '(student: Student) => void' を型 '(person: Person) => void' に割り当てることはできません。
// パラメーター 'student' および 'person' は型に互換性がありません。
// プロパティ 'club' は型 'Person' にありませんが、型 'Student' では必須です。
// OK
fn4 = fn3
// fn4のパラメータの型はStudent型のため、Student型のオブジェクトを渡す必要がある。
fn4({ name: 'John', age: 20, club: 'Soccer' }) // OK
// fn4({ name: 'John', age: 20 }) // NG
// 型 '{ name: string; age: number; }' の引数を型 'Student' のパラメーターに割り当てることはできません。
// プロパティ 'club' は型 '{ name: string; age: number; }' にありませんが、型 'Student' では必須です。 上記の例では、fn3とfn4は、どちらも同じ数のパラメータを持ち、戻り値はvoid型です。異なるのはパラメータの型だけで、fn3のパラメータはPerson型であり、fn4のパラメータはStudent型。パラメータの互換性を確認すると、「fn4のパラメータの型(Student) <: fn3のパラメータの型(Person)」という関係にあるため、fn4をfn3に代入することはできない。しかし、その逆は互換性の条件を満たすので代入可能。パラメータ名は同じである必要がある。
fn4 = fn3の代入後、fn4を呼び出す際には、Student型のオブジェクトを引数として渡す必要がある。このとき、fn4にはfn3が代入されているため、fn3に引数が渡され実行される。fn3に渡される引数にはPerson型にはないclubプロパティが含まれるが、関数の内部で使用していないので無視しても問題ない。
仮に、互換性を判定する条件としてのパラメータの型関係が逆になっていたとすれば、期待されるプロパティを持つオブジェクトが渡されることが保証されなくなり、安全でなくなることがわかる。例として、fn3 = fn4が許されれば、fn3を呼び出すと、fn3に代入されているfn4に、Person型の引数が渡ることになる。
パラメータの数と関数型の互換性
interface Person {
name: string
age: number
}
let fn3 = (person: Person) => {
console.log(`That person's name is ${person.name} (${person.age})`)
}
let fn5 = (person: Person, gender: string) => {
console.log(`That person's name is ${person.name}(${person.age}), ${gender}`)
}
// NG. fn3のパラメータの数 < fn5のパラメータの数 なので条件を満たさない。
// fn3 = fn5
// 型 '(person: Person, gender: string) => void' を型 '(person: Person) => void' に割り当てることはできません。
// ターゲット署名の引数が少なすぎます。2 以上が必要ですが、1 でした。
// OK
fn5 = fn3
// fn5は関数型としてパラメータを2つ持つため、引数を2つ渡す必要がある。
fn5({ name: 'John', age: 20 }, 'fmale') // OK
// NG
// fn5({ name: 'John', age: 20 })
// 2 個の引数が必要ですが、1 個指定されました。 上記の例では、fn5は、fn3と同じPerson型のパラメータに加えて、string型のパラメータを持つ。どちらの関数も戻り値はvoid型で、異なるのはパラメータの数だけ。互換性を確認すると、fn5のパラメータ数がfn3より多いため、fn5をfn3に代入することはできない。その逆は互換性の条件を満たすので代入可能。
fn5 = fn3として、fn5を実行する際は、Person型の引数に加えて、string型の引数も渡す必要がある。それらの引数は、fn5に代入されているfn3に渡り実行される。fn3のパラメータにはないstring型の引数が渡ってくるが、この引数は無視される。この引数は関数の内部で使用していないので無視して問題ない。
仮に互換性に関するパラメータの数の関係が逆になっていたら、必要な引数が渡ってくることが保証されなくなり安全でなくなる。
関数型の互換性
interface Person {
name: string
age: number
}
// インターフェイスの拡張によって、自動的にStudentはPersonのサブタイプになる
interface Student extends Person {
club: string
}
// Studentインターフェイスの構造
// {
// name: string
// age: number
// club: string
// }
let fn3 = (person: Person) => {
console.log(`That person's name is ${person.name} (${person.age})`)
}
let fn6 = (student: Student, gender: string) => {
console.log(
`That student's name is ${student.name} (${student.age}), ${gender} and enjoys ${student.club}`,
)
}
// OK
fn6 = fn3
// fn6のパラメータの型 <: fn3のパラメータの型
// fn3のパラメータの型 <: fn6のパラメータの型 上記の例では、fn6は、fn3と異なる型のパラメータを持ち、さらにパラメータの数も異なるが、fn3をfn6に代入することは可能。これは、fn3のパラメータの型がfn6のそれに対するスーパータイプであり、fn3のパラメータの数がfn6のそれより少ないため。
関数Aと関数Bにおいて、次の条件がすべて満たされる場合、関数Bは関数Aのサブタイプとなる。
- 各パラメータにおいて、「Aのパラメータの型 <: Bのパラメータの型」
- Bのパラメータ数 <= Aのパラメータ数
- Bの戻り値の型 <: Aの戻り値の型