【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の基本的な型については以下を参考にしてみてください。
応用編は以下をどうぞ。