【TypeScript】Generics(ジェネリクス)
Generics とは
Generics とは、Java や C#と同じく<>
を使って型をパラメーター化します。
実際にどのような型であるかは、使用する際に指定します。
これだけではわからないと思うので、具体的な例をあげてみます。
まずは配列の宣言を思い出してください。
例えば文字列の配列を宣言する場合、string[]
かArray<string>
とします。
見ての通り、Array
で宣言する場合は<>
で指定した型の要素を持つようになります。
言い換えると、宣言するまではどのような型になるかはわからないということになります。
ではこのArray
と同じMyArray
という型を定義してみます。
(正確にはArray
と同じではありません。)
type MyArray<T> = {
[key in number]: T
}
const ary1: MyArray<number> = [1, 2, 3]
const ary2: MyArray<string> = ['a', 'b', 'c']
const ary3: MyArray<boolean> = [true, false, true]
T
は関数でいうところのパラメーター(引数)だと思ってください。
MyArray<number>
とするとT = number
となり、[key in number]: number
となるため、number
型の値を持つ配列(オブジェクト)型となります。
このように、Generics を使うことで動的な型定義が可能になります。
Generics の基本
定義
上述した通りですが、Generics のは<>
を用いて定義します。
<>
内に使用する文字は任意ですが、T
(Type)、U
(Unknown)、V
(Value)、K
(Key)などの大文字が一般的に使われます。
type MyType<T> = {
value: T
}
const v1: MyType<number> = { value: 0 } //OK
const v2: MyType<string> = { value: 'test' } //OK
const v3: MyType<boolean> = { value: 1 } //NG
初期設定
以下のようにすると、Generics を省略した場合の型を定義することができます。
type MyType<T = string> = {
value: T
}
const v1: MyType = { value: 'test' } //OK
const v2: MyType<number> = { value: 0 } //OK
const v2: MyType = { value: 0 } //NG
extends
extends
は、Generics に指定できる型を制限することができます。
以下は、number
とstring
のみを指定できるようにした例です。
type MyType<T extends number | string> = {
value: T
}
const v1: MyType<number> = { value: 0 } //OK
const v2: MyType<string> = { value: 'test' } //OK
const v3: MyType<boolean> = { value: true } //NG: booleanは指定不可
複数の Generics
Generics は、カンマで区切ることで複数指定することができます。
type MyType<T1, T2> = {
value1: T1
value2: T2
}
const v: MyType<number, string> = { value1: 0, value2: 'test' }
また以下のように、Generics 内で組み合わせることも可能です。
type MyType<T1, T2 = T1> = {
value1: T1
value2: T2
}
const v: MyType<number> = { value1: 0, value2: 1 }
関数の Generics
関数定義における Generics は、関数名に続けて指定します。 以下に簡単な例をあげます。
function createArray<T>(size: number, initial: T): Array<T> {
const ary: Array<T> = []
ary.length = size
ary.fill(initial)
return ary
}
const ary = createArray<number>(3, 1) //[1, 1, 1]
要素数がsize
、すべての要素の値をinitial
に設定した配列を作る関数になります。
関数の Generics を使用することで、どの型の配列か作成するかを指定しています。
実際にこの関数を使用する際は、Generics の指定は不要です。
なぜなら、引数のinitial
に指定した値から型推論をしてくれるからです。
const ary = createArray(3, 'test') //['test', 'test', 'test']
もう 1 つ例をあげます。
function getType<T extends object, K extends keyof T>(obj: T, key: K) {
return typeof val[key]
}
const obj = {
x: 1,
y: 'test',
z: true,
}
const tx = getType(obj, 'x') // 'number'
const ty = getType(obj, 'y') // 'string'
const ta = getType(obj, 'a') // NG
これは指定したオブジェクトのキーの型を返す関数になります。
ここでのポイントは、extends
を用いることで引数に設定できる型(値)を制限していることです。
K
には、keyof T
としていることから、T
の持つキーのリテラルのみ指定できるようになります。
例の場合は、'x'
、'y'
、'z'
のみ指定可能です。
クラスの Generics
Generics は、以下のようにクラスでも使用できます。 これについては深くは言及しません。
class MyClass<T> {
constructor(public value: T) {
this.value = value
}
}
const c1 = new MyClass<number>(1)
const c2 = new MyClass('test') //型推論もしてくれる
Conditional Types
Conditional Types とは
Conditional Types とは、型の条件分岐を意味します。
A extends B ? C : D
という構文で表され、「型 A が型 B と互換性があれば型 C、なければ型 D とする」という意味になります。
まずは簡単な例をあげます。
type IsString<T> = T extends string ? true : false
type T1 = IsString<string> //true
type T2 = IsString<number> //false
IsString
は「T
がstring
またはstring
のリテラルであればtrue
、そうでなければfalse
のリテラル型とする」という型になります。
これ自体は実用性のないものかもしれないので、具体的な例を使用例あげていきます。
ユニオン型の抽出
TypeScript のユーティリティ型の中に、Exclude<T, U>
とExtract<T, U>
というものがあります。
これはユニオン型に対して、それぞれ「T
からU
を除外した型」、「T
とU
で一致する型」を作成します。
type T1 = 'a' | 'b' | 'c'
type T2 = 'a' | 'c' | 'd'
type T3 = Exclude<T1, T2> // 'b'
type T4 = Extract<T1, T2> // 'a' | 'c'
このExclude<T, U>
とExtract<T, U>
を Conditional Types を用いて定義してみます。
type MyExclude<T, U> = T extends U ? never : T
type MyExtract<T, U> = T extends U ? T : never
同じ値を指定すれば、同じ結果が得られるはずです。 少しわかりにくいかもしれないので具体的に値を入れて説明します。
MyExclude<T1, T2>
とした場合、単純に展開すると以下のようになります。
'a' | 'b' | 'c' extends 'a' | 'c' | 'd' ? never : 'a' | 'b' | 'c'
しかし、実際にはこのように展開されません。 ユニオン型の場合は、以下のようにそれぞれの型に対して Conditional Types が適用されます。
('a' extends 'a' | 'c' | 'd' ? never : 'a')
| ('b' extends 'a' | 'c' | 'd' ? never : 'b')
| ('c' extends 'a' | 'c' | 'd' ? never : 'c')
これを判定すると、never | 'b' | never
という結果が得られます。
ユニオン型ではnever
は無視されるため、結果としてb
が得られます。
MyExtract<T, U>
も同様のため、説明は割愛します。
型指定によるプロパティ抽出
オブジェクト定義から指定した型に該当するキーを取得するMatchTypeKeys<T, U>
を作成してみます。
type MatchTypeKeys<T extends object, U> = {
[K in keyof T]: T[K] extends U ? K : never
}[keyof T]
interface I {
a: number
b: string
c: number
}
type T1 = MatchTypeKeys<I, number> //'a' | 'c'
type T2 = MatchTypeKeys<I, string> //'b'
type T3 = MatchTypeKeys<I, boolean> //never
Maped Types を使い、プロパティの型(T[K]
)が指定した型(U
)であればキー(K
)を、そうでなければnever
に置き換えています。
MatchTypeKeys<I, number>
であれば以下のようになります。
type Tmp = {
a: 'a'
b: never
c: 'c'
}
あとは[keyof T]
によって、プロパティ値のユニオン型を作成します。
上述したように、never
は無視されるため'a' | 'c'
となります。
infer
以下のように、配列要素の型を取得するElementType<T>
を定義します。
type ElementType<T> = T extends Array<infer U> ? U : never
type T1 = ElementType<number[]> //number
type T2 = ElementType<string[]> //string
注目すべきはinfer U
という表記です。
上記であれば、「配列要素の型をU
として参照する」という意味になります。
infer
は、型に含まれる部分的な型を参照する目的で使用します。
もう 1 つ例をあげてみます。
ユーティリティ型の 1 つに、関数の戻り値型を取得するReturnType<T>
というものがあります。
type Func = (x: string, y: number) => boolean
type T = ReturnType<Func> //boolean
これをinfer
を使って定義してみます。
type MyReturnType<T> = T extends (...args: any[]) => infer U ? U : never
ここでは、関数型の中にある戻り値の型を参照したいので、() => infer U
と表記しています。
これで同じ結果が得られるはずです。