Michi's Tech Blog

一人前のWebエンジニアを目指して

TypeScriptによる静的型付け言語入門② 『型引数とジェネリクス』

こんにちは!
スマレジ テックファームのMichiです!

前週に引き続き、TypeScriptのキャッチアップをしていきます。今回は『型引数とジェネリクス』についてです。

型引数(ジェネリック型)

型引数とは、型を定義するときに引数を持たせることのできる機能で、ある型定義を色々な場面でで使いまわしたいときに役立ちます。この型引数を持つ型をジェネリックと呼びます。

ジェネリック型の宣言

ジェネリック型を宣言するときは、type文で型名を書いた直後に<T> のような形で型引数を宣言します。この型引数Tは外側から値を受け取り、受け取った値はtype文の中で変数として使えるようになります。(一般的な関数の引数と同じです)

次はジェネリックFamilyを宣言している例です。

type Male = "male";
type Female = "female";

type Family<T> = {
  father: Male;
  mother: Female;
  child: T;
};

※今回は便宜的に、文字列maleのみを許容する型Maleと、文字列femaleのみを許容する型Femaleを定義しています。

fatherMale型、motherFeMale型ですが、childはこの段階ではわからないので、型引数Tを引き当てています。

型引数は複数宣言することもできます。その場合も関数と同じように、カンマ区切りで<T, S>のように宣言します。

type Family<T, S> = {
  father: Male;
  mother: Female;
  first_child: T;
  second_child: S;
};

ジェネリック型を使用する

では、上で定義した型引数を使ってみましょう。

ジェネリック型を使用するには、宣言時と同じく<>という記号を使って引数を渡します。

次は、スミスさん家の家族構成をジェネリック型を使って定義したときの例です。スミスさん家では上の子供は男の子、下の子供は女の子であるとします。

const smith: Family<Male, Female> = {
  father: "male",
  mother: "female",
  first_child: "male",
  second_child: "female",
};

この例では、先ほど宣言したFamily型のfirst_childにはMale型を、second_childにはFemale型を渡しています。よって、first_child, second_childの値にそれぞれmale, femaleが入ることは型チェック上問題ありません。

逆に、次のコードはエラーとなります。

const smith: Family<Male, Female> = {
  father: "male",
  mother: "female",
  first_child: "male",
  second_child: "male",
};

// エラー: Type '"male"' is not assignable to type '"female"'.

型引数で2番目に受け取る型はFemale型と定義しているにもかかわらず、second_childプロパティの実際の値はmale(Male型)です。型引数で定義している型と、実際のプロパティに入っている型が異なるため、「型が違う」という内容のエラーが発生します。

オプショナルな型引数

型引数の宣言時には、オプショナル(省略可能)な型引数を宣言することもできます。

オプショナルな型引数を宣言するためには、 型引数の後ろに= デフォルト型といった表記で、デフォルトの型を設定します。

type Family<T = Male, S = Female> = {
  father: Male;
  mother: Female;
  first_child: T;
  second_child: S;
};

上の例では、デフォルト型としてT = Male型, S = Female型をそれぞれ設定しています。

オプショナルとして宣言された型引数は、使用時に型の宣言を省略することができます。

const smith: Family<Male> = {
  father: "male",
  mother: "female",
  first_child: "male",
  second_child: "female",
};

上の例では型引数Sに値を渡していませんが、デフォルトの設定値にしたがって、second_child = Female型となります。

尚、これの逆で、最初の型引数Tのみを省略することはできません。これも通常の関数の引数と同じです。

const smith: Family<Female> = {  // 一番目の引数`T`に`Female`型を割り当てるという処理になってしまう。
  father: "male",
  mother: "female",
  first_child: "male",
  second_child: "female",
};

// エラー: Type '"male"' is not assignable to type '"female"

ジェネリクス

ジェネリクスとは、型引数を受け取る関数を作る機能のことです。先ほどのジェネリック型を関数に適用したものですね。

この型引数を持つ関数のことをジェネリック関数と呼びます。

例えば、数値の配列を引数に取り、その最初の要素を返す関数を考えてみましょう。

function getFirstNumber(array: number[]): number {
    return array[0];
}

このとき、引数はnumber[]型、返り値はnumber型と指定されています。なので、次の1番目の処理は結果が返ってきますが、2番目はエラーとなります(当たり前ですね)。

const num = getFirstNumber([1, 2, 3]);
// 結果: 1
const str = getFirstNumber(["a", "b", "c"]);
// エラー: Type 'string' is not assignable to type 'number'.

ですが、引数と返り値の型が異なるたびに、同じ処理を実行する関数をいくつも作らなければならないのは面倒です。

そこで、関数の呼び出し時に必要な型情報を外側から与えてやるのがジェネリクスの機能です。

ジェネリック関数の宣言

ジェネリック関数を宣言するには、ジェネリック型の時と同じく、<T>のような形で型引数を宣言します。

次の例では、特定の型の配列を引数に取り、その最初の要素を返す関数を定義しています。

■function命令で定義する場合

function getFirstElement<T>(array: T[]): T {
    return array[0];
}

■無名関数で定義する場合

const getFirstElement = function <T>(array: T[]): T {
    return array[0];
}

■アロー関数で定義する場合

const getFirstElement = <T>(array: T[]): T => {
    return array[0];
}

いずれの場合も、型引数が実引数の直前に置かれています。また、もちろん型引数は複数を取ることも可能です。

ジェネリック関数の使用

ジェネリック関数を使用する(呼び出す)ときも、<>という記号を使って型引数を渡します。(もうお馴染みになってきましたね)

次の例では、実引数に[1, 2, 3]を渡すときは型引数にnumber型を、実引数に["a", "b", "c"]を渡すときは型引数にstring型を指定しています。

const num = getFirstElement<number>([1, 2, 3]);
// 結果: 1
const str = getFirstElement<string>(["a", "b", "c"]);
// 結果: "a"

これにより、関数により汎用性を持たせることができました。

型引数の省略(型推論

先ほどのコードは、関数呼び出し時の型引数を省略しても動きます。

const num = getFirstElement([1, 2, 3]);
// 結果: 1
const str = getFirstElement(["a", "b", "c"]);
// 結果: "a"

これは、TypeScriptの型推論という機能が働いているからです。

例えば、実引数に[1, 2, 3]が入ってきた場合、TypeScriptはこれをnumber[]型だと判断し、型引数Tにはnumber型をあてはめます。返り値は数値の1number型)になるので、この推論に矛盾はありません。

このように、型推論の機能によって、ほとんどの場合は型引数を明示的に指定しなくても、ジェネリック関数を使うことが可能です。

まとめ

以上、TypeScriptの『型引数とジェネリクス』についてのキャッチアップでした。

7月にはTypeScript(React)の案件が始まるので、今のうちにできることはどんどん進めていきたいです。