TypeScriptの型の互換性について

今回はTypeScriptの型の互換性について取り上げます。

型の互換性・・・??

簡単に言うと例えば、ある型の変数であるHogeに、別の型であるFugaが代入できれば、型の互換性があると言えます。逆に代入できなければ型の互換性がないとなります。

この型の互換性がどのように決まるかは言語によって異なります。TypeScriptの場合は、構造的部分型(Structural Subtyping)が採用されています。

言葉だけで捉えるとなんだか難しそうですね・・・ 実際にコードを見て確認します。

interface Tree {
  age: number
}

class Person {
  age: number
}

const tanaka: Tree = new Person();
tanaka.age = 20;
console.log(tanaka.age); // 20

上記のようにPersonクラスがTreeを実装していなくてもメンバーが同じなのでコンパイルエラーにはならないのです。 これを構造的部分型(Structural Subtyping)というのです。

一方でC#Javaの場合は、PersonクラスがTreeインターフェースを実装していないといけないので上記のコードではエラーになります。(自分はC#Javaを触ったことがないので別の機会でお試しください。。。) このようなものを名目上の型付けと呼びます。

また以下の例はどうでしょうか?

interface User {
  name: string;
}

let x: User;
let y = { name:"hoge", address: "Tokyo" }
x = y;

こちらは正常にコンパイルされます。 変数yが変数xのプロパティ(ここでいうとname)をもっているので割り当てが可能になります。

逆にyxを代入した時はどうでしょうか?

interface User {
  name: string;
}

let x: User;
let y = { name:"hoge", address: "Tokyo" }
y = x;

お察しかもしれませんが、yに対応するプロパティをxnameしか持っていないのでコンパイルエラーになります。

関数の場合はどのように型を評価しているのでしょうか?

以下のような関数があります。

let hoge = (a: number) => 0;
let fuga = (b: number, c: string) => 0;
fuga = hoge;

結果としては、fugaにhogeを割り当て可能です。注意する点としては、引数の名前が異なっていても型のみが考慮されるのです。 hogeの関数の引数名はaですが、fugaの関数の引数名はbcです。 上記の例でみたオブジェクトの例では、プロパティ名が異なっていると正しく割り当てされません。

またfuga = hogeのように引数が切り捨てられるのが許される理由としては、関数の引数が無視されることがJavaScriptでよくみられるからだそうです。

例えば、以下の例です。

const names = ["田中", "佐藤", "中村"];
const result = names.some(name => name === "田中"); // true

someメソッドのコールバック関数のパラメーターは以下の3つを受け取ります。

  • 配列の中の値
  • 各要素のインデックス
  • 要素を格納している配列

このパラメーターを全て使う時もあるとはありますが、使わないケースも多いです。このことから引数の切り捨てられるのが許されるようです。

まとめ

このようにTypeScriptには構造的部分型が取り入れられています。オブジェクトや関数などによって型の評価の仕方が異なるのです。 次回はジェネリクスのことについて取り上げたいと思います。