始めに
皆さんこんにちは!新卒3年目エンジニアの細川です。
今回はTypeScriptの型ジェネリクスについてまとめていこうと思います。
TypeScriptを書いてて、<T>
とかよく見るけど何それ?という方は是非読んでみてください!
型ジェネリクスって?
型ジェネリクスは、型を変数として受け取り、利用するという仕組みです。
皆さんも以下のような<T>
とか<K>
の記述をよく目にすることがあるかと思います。
type EchoType<T> = T
type StringType = EchoType<string> // string型
これが型ジェネリクスです。Tという型変数を受け取り、それを右辺で利用することができます。この例では受け取った型をそのまま代入しているので、渡した型そのままとなります
T
の部分は引数の名前なので、なんでもいいのですが、慣例的にT
やK
などがよく使われます。
関数などの引数と同様に、複数の型を受け取ることもできます。
type UnionType<K, T> = K | T
type StringOrBooleanType = UnionType<string, boolean> // string | boolean型
型のみ違う関数への利用
サバイバルTypeScriptで利用方法として紹介されているものに、”型のみ”が異なる関数の実装を一つにまとめられるというのがあります。サンプルを見てみましょう。
まず、以下のような、五分五分の確立で1つめの引数か2つ目の引数のどちらかを返す関数を考えてみます。
const chooseRandomlyString = (v1: string, v2: string): string => {
return Math.random() <= 0.5 ? v1 : v2;
}
この関数を使う場合は以下のように記述します。
const winOrLose = chooseRandomlyString("勝ち", "負け");
console.log(winOrLose);// 勝ち or 負け
次に、文字列だけでなく、数値の抽選も行う必要が出てきた場合のことを考えてみます。
この場合、chooseRandomlyNumberを作る必要があります。
// 数値用の抽選関数
const chooseRandomlyNumber = (v1: number, v2: number): number => {
return Math.random() <= 0.5 ? v1 : v2;
}
const num: number = chooseRandomlyNumber(1, 2);
console.log(num); // 1 or 2
このように内部の実装が同じでも型が違うだけで複数関数を作成しなければならない場合に型ジェネリクスを利用することができます。
const chooseRandomly = <T,>(v1: T, v2: T): T => {
return Math.random() <= 0.5 ? v1 : v2;
}
chooseRandomly<string>("勝ち", "負け");
chooseRandomly<number>(1, 2);
このように型変数Tを受け取ることで、一つの実装で様々な型に対応できるようになります。
注意点として、上記のようなアロー関数で記述する場合<T,>
のカンマを忘れるとエラーが出る点に注意してください。
応用編
この型ジェネリクスを応用することで、様々な便利な型を作成することができます。
例えば、以下のようにある型のundefined許容の型を作ったり、配列にしたり、型をより抽象的に定めることもできるようになります。
// undefined許容に変換
type Optional<T> = T | undefined
// その型の配列型に変換
type ArrayType<T> = T[]
// 受け取った型のvalue1, value2というプロパティを持つ型
type Box<T> = {
value1: T;
value2: T;
}
const numberBox: Box<number> = { value1: 42 , value2: 2};
const groupBox: Box<string[]> = { value1: ["apple", "banana"], value2: ["hoge"]};
実際には他にMappedTypesなどと組み合わせて、より強力な型を作成可能ですが、それは後程MappedTypesについての記事を書く際に紹介しようと思います。
extendsについて
extends
を用いることで、受けとる型変数を制限することもできます。
元々extends
自体はinterfaceの継承で使うものというイメージが強く他言語でも目にする機会もあると思います。(例:Javaのextends)
以下に基本的なextends
の使い方を示します。
interface People {
name: string
age: number
}
// Peopleを継承したEmployeeを作成
interface Employee extends People {
companyName: string
employeeNumber: number
}
// Peopleのプロパティに加えて、Employeeで追加したプロパティを持つ
const emp1: Employee = {
name: "Taro", // Peopleのプロパティ
age: 25, // Peopleのプロパティ
companyName: "SIOS", // Employeeのプロパティ
employeeNumber: 12345 // Employeeのプロパティ
}
このような使い方は皆さんも馴染み深いものではないでしょうか?
このextends
を使うと、型ジェネリクスで受け取る型を制限することもできます。
使い方としては以下の通りです。
type MyString<T extends string> = T
// MyStringは型変数としてstringしか受け取れない
const mystr: MyString<string> = "mystr"
T
がextends
の後ろの型を継承するようになり、例の場合ではstring型しか渡せなくなります。
渡そうとすると以下のようにコンパイルエラーとなります。
このように、型定義の型引数の名前を定義する部分にextends
を追加することで、型引数として受け取る型を制限することができます。
TypeScriptの組み込みの型には結構extends
も利用されています。もし型の実装の中にextends
を見つけたら、受け取る型を絞っているんだなと思いましょう。
まとめ
- 型ジェネリクスは引数として型を受け取って変数のように利用できる仕組み
type UnionType<K, T> = K | T
type StringOrBooleanType = UnionType<string, boolean> // string | boolean型
- extendsを使うと受け取る型に制約を持たせられる
type MyString<T extends string> = T
// MyStringは型変数としてstringしか受け取れない
const mystr: MyString<string> = "mystr"
終わりに
今回はTypeScriptの型ジェネリクスについてまとめました。
知っているとなんともないですが、知らずに見ると「難しそう!」と感じてしまうのではないでしょうか?
この記事で少しでも型ジェネリクスへの理解が進めば幸いです。
型ジェネリクスを応用すると非常に強力で柔軟な型もたくさん作れるので、追々紹介したいと思います!
他にもTypeScriptのあれこれについてまとめていますのでぜひ読んでみてください!