【TypeScript】ジェネリクスの基本を理解する

【TypeScript】ジェネリクスの基本を理解する

TypeScriptにおけるジェネリクスは、使われるまで型が決まらず、実際に使うときに値と一緒に型も渡すことで型を動的にすることができます。

基本的な記述

ジェネリクスを使用しない場合の関数

以下はジェネリクスを使用せず関数を定義した例です。パラメーターや返り値に直接型を指定しています。

const count = (num: number): number => {
  return num;
};

console.log(count(20));

ジェネリクスを使用した関数

count関数の呼び出し時に、引数と一緒に型が渡ってきます。

const count = <T>(num: T): T => {
  return num;
};

console.log(count<number>(10)); // 呼び出し時に型が渡される

引数の方が明確な場合、型推論も適用されます。

const count = <T>(num: T): T => {
  return num;
};

console.log(count(10)); // 型推論も適用される 

<>の中にはどんな文字列でも入れることが可能です。
基本的に大文字アルファベットを入れるのが御作法のようです。
以下は例です。

  • T … Type(型)
  • S、U … 2,3番目の引数
  • E … Element(要素)
  • K … Key(キー)
  • V … Value(値)
  • N … Number(数値)

extendsを使って型パラメータに制約をつける

型変数 extends 代入を許可する型(例: number)のように型に制約をつけることができます。

const func = <T extends number>(num: T): number => {
  return num
}

console.log(func(1));// 1
console.log(func('hello'));// エラー

ユニオン型で複数指定も可能です。

const func = <T extends number | string>(value: T) => {
  return value;
};

console.log(func(1));// 1
console.log(func('hello'));// hello
console.log(func(ture));// エラー

また、ジェネリクスは呼び出されるまで型が確定しないため、「number型で使えるはずの +, – 等の演算子が使えない」という状況が出てきます。
それらをextendsで解決することができます。以下は例です。

const score = <T, U extends number>(name: T, numLang: U , numMath: U): string => {
  return `テストの結果
  名前:${name}、
  国語:${numLang}点、
  算数:${numMath}点、
  合計:${numLang + numMath}点`;
};

console.log(score('taro', 65, 72));
// テストの結果
//   名前:taro、
//   国語:65点、
//   算数:72点、
//   合計:137点

引数が配列の場合

引数に配列を取る場合は以下のように記述できます。

const arr = <T>(arr: T[]): T[] => {
  return arr;
};

arr<number>([1, 2, 3]);

ユニオン型で指定

const arr = <T>(arr: T[]) => {
  console.log(`名前:${arr[0]}、年齢:${arr[1]}`);
};

arr<string | number>(['山田', 22]); // 名前:山田、年齢:22

タプル型で指定

const arr = <T extends [string, number]>(arr: T) => {
  console.log(`名前:${arr[0]}、年齢:${arr[1]}`);
};

arr<[string, number]>(['山田', 22]); // 名前:山田、年齢:22

引数がオブジェクトの場合

const obj = <T extends { name: string; age: number }>(person: T) => {
  console.log(`名前:${person.name}、年齢:${person.age}`);
};

obj({ name: '山田', age: 22 }); // 名前:山田、年齢:22

typeエイリアスやinterfaceにジェネリクス使用する

typeエイリアスの例

type Obj<T> = {
  name: string;
  score: T[];
};

const person: Obj<number> = {
  name: '山田',
  score: [10, 20, 30],
};

interfaceの例

interface Person<T, U> {
  name: T;
  age: U;
}

const person = <T extends Person<string, number>>(person: T) => {
  console.log(`名前:${person.name}、年齢:${person.age}`); 
};

person({ name: '山田', age: 20 }); // 名前:山田、年齢:22

Promiseに対してジェネリクスを使用する

Promiseを格納する変数に型を指定する場合

const hello: Promise<string> = new Promise((resolve) => {
  setTimeout(() => {
    resolve('hello');
  }, 1000);
});

hello.then((data) => {
  console.log(data.toUpperCase()); // HELLO
});

Promiseに直接型を指定する場合

const hello = new Promise<string>((resolve) => {
  setTimeout(() => {
    resolve('hello');
  }, 1000);
});

hello.then((data) => {
  console.log(data.toUpperCase());// HELLO
});