ts.svg

【TypeScript】Generics(ジェネリクス)

TypeScript

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 に指定できる型を制限することができます。 以下は、numberstringのみを指定できるようにした例です。

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は「Tstringまたはstringのリテラルであればtrue、そうでなければfalseのリテラル型とする」という型になります。 これ自体は実用性のないものかもしれないので、具体的な例を使用例あげていきます。

ユニオン型の抽出

TypeScript のユーティリティ型の中に、Exclude<T, U>Extract<T, U>というものがあります。 これはユニオン型に対して、それぞれ「TからUを除外した型」、「TUで一致する型」を作成します。

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と表記しています。 これで同じ結果が得られるはずです。