はじめに
筆者はTypeScript初学者です。学習する中で、「型 'XXX' を型 'YYY' に割り当てることはできません。」というようなエラー文と対峙する機会が多いと思いました。これはTypeScriptの型付けシステムを理解することで、なんとなく解消していたエラー文への理解が深まったり、腑に落ちることがありました。初学者が最初に知っておくと今後の学習や開発がスムーズに進むのではないかと思い、TypeScriptの型付けシステムについて学んだことをこの記事にまとめようと思います。
ここでは、スーパータイプ、サブタイプの考え方から理解を深めていこうと思います。
TypeScriptの型付けアプローチ
多くのプログラミング言語では、型と型の関係性は階層関係で捉えることができます。階層構造において、上位に位置する型を基本型(supertype)と言い、階層構造の下位に位置する型を部分型(subtype)と呼びます。
「型 'XXX' が型 'YYY' に割り当てられる」ということは「型'YYY'は型'XXX'のサブタイプである」ことを指します。
TypeScriptでは構造的部分型と呼ばれるアプローチをとっています。TypeScript以外の言語では、公称型と呼ばれる、型の名前に基づいて型区別し、型同士の関係を決めるようなアプローチをとっている言語が多くありますが、構造的部分型の特徴は、型の構造に基づいて型区別し、型同士の関係を決めていることです。
構造的部分型
JavaScriptの型付けアプローチは構造的型付けであり、その特徴からダックタイピングとも呼ばれています。
ダックタイピングでは、オブジェクトのプロパティやメソッドがどのようなものか、つまりオブジェクトの構造をみてオブジェクトの型を判断します。
ダックタイピングは以下の有名な一文に由来しているようです。
"If it walks like a duck and quacks like a duck, it must be a duck"
(もしもそれがアヒルのように歩き、アヒルのように鳴くのなら、それはアヒルに違いない)
TypeScriptはJavaScriptを拡張した言語であり、ダックタイピングに適した方式である構造的部分型を採用しています。構造的部分型も同じようにオブジェクトの構造をみて型を推測し、割り当て可能かを判断します。
以下の例では、whiskey
はTypeScriptに{name: string; percentage: number; category: string;}
と推論されます。Alcohol
型にはcategory
プロパティは存在していませんが、エラーは出ず問題なくコンパイルできます。これは、whiskey
の型がAlcohol
型のサブタイプであるからです。TypeScriptでは、ある型'XXX'の最小限の条件満たしている型'YYY'のオブジェクトには、型'XXX'を割り当てることができます。
interface Alcohol {
name: string;
percentage: number;
}
const whiskey = { // whiskey: {name: string; percentage: number; category: string;}と推論
name: "hakusyu",
percentage: 40,
category: "distilled"
};
const getName = (drink: Alcohol) => drink.name;
getName(whiskey); // whiskeyにAlcohol型が割り当てられ、エラーはない
上記の例では、説明のために「余剰プロパティチェック」を避けるため、以下例のようにオブジェクトリテラルを定義している値に対して直接型注釈をつけていません。TypeScriptには構造的部分型の考えとは別で、型に存在しないプロパティを禁止するチェック機能があります。
const whiskey: Alcohol = {
name: "hakusyu",
percentage: 40,
category: "distilled", // エラー:「オブジェクト リテラルは既知のプロパティのみ指定できます。'category' は型 'Alcohol' に存在しません。」
};
反対に、以下のようにAlcohol
型がサブタイプとなってしまう場合は、コンパイルエラーとなり、型を割り当てることができません。
interface Alcohol {
name: string;
percentage: number;
category: string;
}
const whiskey = {
name: "hakusyu",
percentage: 40,
};
const getName = (drink: Alcohol) => drink.name;
getName(whiskey); // エラー:「型 '{ name: string; percentage: number; }' の引数を型 'Alcohol' のパラメーターに割り当てることはできません。...」
TypeScriptの型階層構造
TypeScriptにはさまざまな型が用意されていますが、それぞれの型の関係性も定義されています。以下の図では、unknown
側が上位の型、つまりスーパータイプで、never
側が下位の型、つまりサブタイプになります。
unknown型とnever型
unknown型は最上位の型で、すべての型のスーパータイプになり、どの型も割り当てることができます。集合に当てはめると全体集合になるイメージです。対してnever型は、最下位の型となりどの型も割り当てることができません。集合で考えると空集合のようなイメージです。以下は検証例です。
// unknown型
let alcohol: unknown;
alcohol = "whiskey";
alcohol = 1992;
alcohol = true;
alcohol = { alcoholName: "whiskey" };
alcohol = null;
alcohol = undefined;
// never型
let softDrink: never;
softDrink = "whiskey"; // 型 'string' を型 'never' に割り当てることはできません。
softDrink = 1992; // 型 'number' を型 'never' に割り当てることはできません。
softDrink = true; // 型 'boolean' を型 'never' に割り当てることはできません。
softDrink = { alcoholName: "whiskey" }; // 型 '{ alcoholName: string; }' を型 'never' に割り当てることはできません。
softDrink = null; // 型 'null' を型 'never' に割り当てることはできません。
softDrink = undefined; // 型 'undefined' を型 'never' に割り当てることはできません。
any型
any型は特殊な型であり、どんな型の代入も許可する型です。これはany型を代入した変数については、コンパイラーが型チェックを行わないためです。型の階層構造からは外れた型と考えられます。
unknown型とany型
ある状況下ではunknown型とany型は同じように振る舞います。しかし、unknown型を割り当てておくと、どんな型が入ってくるかわからない状態で、ある特定の型にしか使えないメソッドを呼び出したりすると、TypeScriptはエラーを出してくれます。
ここで、any型とするとそもそも型チェックをしないので、エラーは出ずスルーされます。unknown型が使える場合は、こちらを使用する方が安全でしょう。
const alcoholAge = "1992年";
const removeUnit = (value: unknown) => {
return value.replace("年", ""); // valueにエラー:「'value''は 'unknown' 型です」
};
removeUnit(alcoholAge);
上記のエラーは型ガードを記述することで解消されます。
const removeUnit = (value: unknown) => {
if (typeof value !== "string") return;
return value.replace("年", ""); // エラーなし
};
プリミティブ型とリテラル型
TypeScriptは、以下の例のようにconst
で宣言すると、値の型は"whiskey"
という文字列のリテラル値と推論されています。階層構造の図で言えば、string literal
にあたります。この型のスーパータイプはstring
(プリミティブ)です。
const alcoholName = "whiskey"; // const alcoholName: "whiskey"と推論
このように、リテラル値を型にすることが可能で、ユニオン型を使用すれば、自由にスーパータイプをつくることができます。以下例の各型の関係は、string > "whiskey" | "vodka" > "whiskey"
となります。vodka
変数の例ではスーパータイプの型を持つalcoholName
変数をサブタイプの型を持つvodka
変数に割り当てようとしているのでエラーになります。
const whiskey = "whiskey";
// whiskeyのスーパータイプになるため、割り当て可能
const alcoholName: "whiskey" | "vodka" = whiskey;
const drinkName: string = alcoholName;
// whiskeyのサブタイプになるため、割り当て不可
const vodka: "vodka" = alcoholName; // vodkaにエラー:「型 '"whiskey"' を型 '"vodka"' に割り当てることはできません」
オブジェクトについて考えてみる
ここで、改めてオブジェクトのサブタイプを考えてみます。以下の例のalcohol2
オブジェクトではエラーなく無事コンパイルできるでしょうか。
const alcohol = {
name: "whiskey",
percentage: 40,
};
const alcohol2: { name: string; percentage: number; category: string } = alcohol;
答えは、エラーが出てコンパイルできません。上記例は、少し書き方は変えていますが、構造的部分型の項で最初に記述しているコード例の構造と全く一緒です。alcohol2
の型はalcohol
の型のサブタイプとなるで、ここでは型を割り当てることができません。
変数のユニオン型の例などをみると、要素の多い方がスーパータイプというイメージがつきがちですが、オブジェクトに関してプロパティの記述は構造の制限を厳しくすることになります。したがって、プロパティが多いほど、制限が厳しくなりサブタイプになります。
おまけ
以下の例は、正しくコンパイルされるでしょうか。
const alcohol = {
name: "whiskey",
percentage: 40,
};
const alcohol2: { name: string; percentage: number | string;} = alcohol;
こちらは問題なくコンパイルされます。プロパティの値の型については、入る型の種類が多いほど、制限は緩くなるため、alcohol2
の型は alcohol
の型のスーパータイプとなり型の割り当てが可能です。
さいごに
TypeScriptの型付けアプローチを知ることで、TypeScriptへの理解が深まったのではないかと思います。関数のサブタイプを考えるともう少し難しかったり、TypeScriptの型の階層構造についても理解できていないところが多々あり、まだまだ学習が必要ではありますが、初めの一歩の内容として初学者にとって有用なものになっていれば嬉しいです。