6
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

型付けから理解するTypeScript

Last updated at Posted at 2024-09-19

はじめに

筆者は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"
(もしもそれがアヒルのように歩き、アヒルのように鳴くのなら、それはアヒルに違いない)

引用:ダック・タイピング - Wikipedia

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側が下位の型、つまりサブタイプになります。

typescriptの部分型関係イメージ.png
※図は以下記事から引用させていただきました。

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の型の階層構造についても理解できていないところが多々あり、まだまだ学習が必要ではありますが、初めの一歩の内容として初学者にとって有用なものになっていれば嬉しいです。

6
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?