• 最終更新日:

【TypeScript】引数と一緒に型を渡す方法 - ジェネリクス

【TypeScript】引数と一緒に型を渡す方法 - ジェネリクス

今回はTypeScriptで引数と一緒に型を渡すジェネリクスについて説明します。関数だけではなく、interfaceやtypeエイリアス、class、Promiseなどにも使えます。

説明する環境は以下です。

  • macOS Catalina v10.15.5
  • Visual Studio Code v1.57.0
  • TypeScript v4.3.5
  • ESLintでエラーチェック
この記事の目次

引数と一緒に型を渡す方法 - ジェネリクス

ジェネリクスとは、定義した段階では抽象的な型で設定しておいて、実際に使うときに値と一緒に型も渡すイメージです。言葉だけだと分かりにくいので具体的な例で説明していきます。

基本的なジェネリクスの書き方

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

/*-- ジェネリクスを使わないで書いた場合 --*/

const simpleNumber =(num1: number): number => {
  return num1;
};

console.log(simpleNumber(10)); 

これをジェネリクスで書くと以下です。simpleNumber関数を読んだときに引数と一緒に型も渡しています。

この<T>の部分はどんなアルファベットが入ってもOKですが、基本的に大文字でT、U、S、Vなどつけることが多いです。以下は例です。

  • T … Type(タイプ)
  • S、U … 2,3番目
  • E … Element(要素)
  • K … Key(キー)
  • V … Value(値)
  • N … Number(数値)
const simpleNumber = <T>(num1: T): T => { //Tの中にはnumberの型が入る
  return num1;
};

console.log(simpleNumber<number>(10)); //ここで引数と一緒に型も渡す

関数を呼び出すときに引数の値が明確なときは、以下のように省略が可能です。

const simpleNumber = <T>(num1: T): T => { 
  return num1;
};

console.log(simpleNumber(10)); //引数がnumber型と明確なので省略できる

ジェネリクスについてまとめると以下。

  • 定義した段階では抽象的な<T>という形で型を定義しておいて、実際に呼び出されるときに型を確定させる。
  • 引数の値の型が明確なときは省略できる。

型が持っているメソッドを使うときの注意点

引数にnumber型とstring型があって、関数で受け取った引数を何らかの値で返すとします。以下のように書いてしまうとエラーになります。

/*-- 間違った例 --*/

const infomation = < T , U >(name:T,num1: U, num2: U):T => {
  return `名前は${name}です。年齢は ${num1 + num2}です`; //Uがnumber型か判断できないため、エラーになる
};

console.log(infomation<string,number>("山田",10, 20));

ジェネリクスは使われる前まで型が確定していないので、Uがnumber型か判断がわかりません。そのためnumber型で使えるはずの演算子が使えません。また返り値にTを設定しても、今回はnameだけではなく他の要素も組み合わさってretuenしているので使えません。(retuenがnameのみならTを設定してもエラーにならない)

使えるようにするにはextendsを使って型を絞ってあげる必要があります。<U>が明確にnumber型だと伝えるためには以下です。そして返り値の型も明確にstringと指定します。(もしくは型推論されるので指定しない)

/*-- OKな例 --*/

const infomation = < T , U extends number>(name:T,num1: U, num2: U):string => {
  return `名前は${name}です。年齢は ${num1 + num2}です`;
};

console.log(infomation("山田",10, 20));

まとめると、string型やnumber型などそれぞれが持っているメソッドを使いたい場合はextendsで抽象的に表現されている<T>などの型を明確にする必要がある、ということ。

引数が配列の関数にジェネリクスを使う

引数に配列がある場合は以下のようにジェネリクスが使えます。

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

dammyArr<number>([10, 20, 30]); 
//↑ここの型は省略できます
//dammyArr([10, 20, 30]); ← OK

配列に複数の型があって、指定した型のいずれかに該当すればOKな場合は以下。

const dammyArr = <T>(arr: T[]) => {
  console.log(`配列の1つ目は${arr[0]}で、2つ目は${arr[1]}です`);
};

dammyArr<(string | number)>(["田中", 20]); 
//↑ここの型は省略できます
//dammyArr(["田中", 20]); ← OK

配列に複数の型があって、指定した順番どおりの型で個数に決まりがあるTuple型の場合は以下。

const dammyArr = <T extends [string, number]>(arr: T) => {
  console.log(`配列の1つ目は${arr[0]}で2つ目は${arr[1]}です`);
};

dammyArr<[string, number]>(["田中", 20]); 
//↑ここの型は省略できます
//dammyArr(["田中", 20]); ← OK

オブジェクトに対してジェネリクスを使う

オブジェクトで使う場合は以下。

const dammyObject = <T extends { name: string; age: number }>(person: T) => {
  console.log(`私の名前は${person.name}で、年齢は${person.age}です。`);
};

dammyObject<{ name: string; age: number }>({ name: "山田", age: 15 }); 
//↑ここの型は省略できます
//dammyObject({ name: "山田", age: 15 }); ← OK

extendsにはtypeエイリアスでも指定できます。

type DammyObject = {
  name: string;
  age: number;
};

const dammyObject = <T extends DammyObject>(person: T) => {
  console.log(`私の名前は${person.name}で、年齢は${person.age}です。`);
};

dammyObject<{ name: string; age: number }>({ name: "山田", age: 15 }); 
//↑ここの型は省略できます
//dammyObject({ name: "山田", age: 15 }); ← OK

interfaceもtypeエイリアスと同じように使えます。

interface DammyObject {
  name: string;
  age: number;
}

const dammyObject = <T extends DammyObject>(person: T) => {
  console.log(`私の名前は${person.name}で、年齢は${person.age}です。`);
};

dammyObject<{ name: string; age: number }>({ name: "山田", age: 15 });
//↑ここの型は省略できます
//dammyObject({ name: "山田", age: 15 }); ← OK

typeエイリアスにジェネリクスを使う

関数だけではなく、typeエイリアにもジェネリクスは使えます。typeエイリアスとして定義したときは型の指定はせず、実際に型として使うときに型をtypeエイリアスに渡しています。

type Human<T> = {
  name: string;
  age: T[];
};

const human: Human<number> = {
  name: "山田",
  age: [10, 20, 30]
};

interfaceにジェネリクスを使う

typeエイリアスと使い方は一緒です。

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

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

person<{ name: string; age: number }>({ name: "山田", age: 20 }); 
//↑ここの型は省略できます
//person({ name: "山田", age: 20 }); ← OK

classに対してジェネリクスを使う

class構文に対して、newでインスタンス化された時に一緒に型も渡しています。

class animals<T, U> {
  constructor(public name: T, public age: U, public gender: T) {}

  info() {
    console.log(`この動物の名前は${this.name}で、年齢は${this.age}、そして性別は${this.gender}です。`);
  }
}

const animal = new animals<string, number>("猿", 10, "オス"); 
//↑ここの型は省略できます
//const animal = new animals("猿", 10, "オス"); ← OK
animal.info();

Promiseに対してジェネリクスを使う

例1と例2の違いは、例1では変数に型を指定していて、例2ではPromiseに対して直接型を指定しています。

/*-- 例1 --*/
const hello: Promise<string> = new Promise((resolve) => {
  setTimeout(() => {
    resolve("hello");
  }, 3000);
});

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

/*-- 例2 --*/
const hello2 = new Promisee<string>((resolve) => {
  setTimeout(() => {
    resolve("hello");
  }, 3000);
});

hello2.then((data) => {
  console.log(data.toUpperCase()); 
});

さいごに

今回はTypeScriptで引数と一緒に型を渡すジェネリクスについて説明しました。使い方によっては非常に便利に型指定ができるようになります。ただし、やみくもに使うとコードの可読性が下がるので注意しましょう。

TypeScriptの基本的な型については以下を参考にしてみてください。

応用編は以下をどうぞ。