😀

TypeScriptの部分型をわかりやすく解説してみた

に公開

はじめに

TypeScriptを学んでいて、部分型という概念について学んだのですが、すっきり整理したいので、本記事を書きます。

この記事の対象者

・TypeScriptを学び始めた人
・部分型の理解が浅い人
・部分型がなぜ重要なのかわからない人

部分型とは

部分型には大きく分けて名前的部分型と構造的部分型の2種類があります。

名前的部分型
名前的部分型は「型どうしの関係を明示的に宣言する」必要があります。
例えばJavaが採用しており、CircleがShapeの部分型であることをextendsによって表現します。

class Shape {}

class Circle extends Shape {}

構造的部分型
一方で TypeScriptは「構造的部分型」を採用しています。
構造的部分型では、型どうしの関係を明示しなくても、持っているプロパティの構造によって部分型かどうかが決まるのが特徴です。
以下の例では、Dog型はAnimal型の部分型とみなされます。
以下のコード

type Animal = {
  name: string;
};

type Dog = {
  name: string;
  breed: string;
};

const dog: Dog = { name: "Pochi", breed: "Shiba" };

const animal: Animal = dog;

部分型という言葉の意味が逆ではないかという話

まず以下のコードを見てください

type Animal = {
  name: string;
};

type Dog = {
  name: string;
  breed: string;
};

const dog: Dog = { name: "Pochi", breed: "Shiba" };

const animal: Animal = dog;

上記の例で言うと、Dog型はAnimal型の部分型です。
初学者の方だと「逆じゃない?」と思われるかもしれません。自分も思いました。
この違和感に関して解説します。
まずAnimal型とDog型で、より具体的な型はどちらでしょうか?
→正解はDog型です。
よって、型定義の抽象度が高いのはAnimal型ということになるので、Dog型はAnimal型の部分型といえます。
集合論的に言うと、Dog型の値の集合はAnimal型の値の集合に含まれるため、Dog型はAnimal型の部分型であるということになります。
犬は動物の中に含まれますから、犬は動物の一部分というようなイメージです。

また、Animal型はnameに対してのみ型を指定しているので、Animalとして扱うときに保証されるのはnameだけです。
したがって以下のように、部分型を指定した変数に、他のプロパティが存在するオブジェクトを代入することができますし、consoleで参照することもできますが、部分型(Animal型)に存在しないプロパティにはアクセスできません。

const dog: Dog = { name: "Pochi", breed: "Shiba" };
const animal: Animal = dog;

// Animal型としては代入できる
console.log(animal); // ✅ 中身は表示される

// Animal型としてはbreedプロパティにはアクセスできない
console.log(animal.breed); // ❌ エラー: Property 'breed' does not exist on type 'Animal'

部分型を指定したのにコンパイルエラーが出る

以下のように、部分型であるAnimal型を指定した変数に、直接Dog型のオブジェクトを入れると、コンパイルエラーとなります。

type Animal = {
  name: string;
};

const animal: Animal = {
  name: "Pochi",
  breed: "Shiba", // ❌ エラー
};

これは「オブジェクトリテラルの直接代入には過剰プロパティチェック(excess property check)が働く」 という仕様があるからです。
ここで出るエラーは「部分型関係が破れてる」からじゃありません。
TypeScriptが「この人、たぶんtypoして余計なプロパティ書いたな?」と推測して、わざわざエラーにしてくれているだけです。
ではこのコンパイルエラーを回避するにはどのようにするのがベストなのか。
以下のように変数経由で代入するのが回避できます。

const dog = { name: "Pochi", breed: "Shiba" }
const animal: Animal = dog // ✅ OK

変数を経由すると「これはもともとDog型として作られた値」とみなされる。
つまり 余分なプロパティがあることは意図的だと判断され、コンパイルOKになる

部分型ってどういう時に便利なの?

以下のコード例を見てください。

type Animal = {
  name: string;
};

function printName(animal: Animal) {
  console.log(animal.name)
}

const dog: Dog = { name: "Pochi", breed: "Shiba" }
printName(dog)

上記のコードでは、printNameという関数を定義しています。
引数はAnimal型です。
このprintName関数を使う際、引数にAnimal型ではなく、Dog型を入れてみるとどうなるでしょうか?
型が違うのでコンパイルエラーが起きると思いますが、実際はコンパイルエラーは起きません。
これが部分型の便利なところです。
つまり、「余分なプロパティを持つ値を、より抽象的な型に渡せる」ことができ、コードの再利用性も上がるのです。
もしも部分型という概念がなければ、型ごとに関数を作る必要が出てしまうでしょう。

最後に

今回は、TypeScript学習の中で引っかかった「部分型」について調べてみました。
言葉の意味が分かりにくかったことに加え、なぜ重要なのかが気になったためです。
学んだ内容を押さえておけば、実務でのコード重複を減らし、より保守性の高い成果物につながると感じました。
説明で間違っている個所があれば指摘していただけますと幸いです。

参考
TypeScript公式
サバイバルTypeScript
プロを目指す人のためのTypeScript入門

Discussion