EY-Office ブログ

TypeScriptのinferを学んだ(複雑な型宣言)

普通にTypeScriptを使ってReactアプリを書いているだけなら複雑な型宣言を目にする事は少ないです。しかしReactや他のライブラリーの型宣言には複雑なものがありますね。

私も複雑な型宣言は得意ではなく、複雑な型宣言が表示されるとそっと閉じてしまう事もままあります。😅

Infer Bing Image Creatorが生成した画像を使っています

複雑な型

前回のブログでkysely-codegenが生成した型定義ファイルを載せましたが、以下の部分は解説しませんでした。このGenerated型はid: Generated<number>のように利用されています。

export type Generated<T> = T extends ColumnType<infer S, infer I, infer U>
  ? ColumnType<S, I | undefined, U>
  : ColumnType<T, T | undefined, T>;

これは複雑ですね、ここには条件型(Conditional Types)infer型演算子 が使われています。

条件型(Conditional Types)

これは型定義に条件を持ち込む機能で、文法はJavaScriptや多くの言語にある条件演算子(三項演算子)と同じく、条件式 ? 条件が真の場合の値(式) : 条件が偽の場合の値(式)です。

わかりやすい簡単な例で説明します、

  • CatNumber型は型パラメーターTを受け取ります、Tがnumber型を継承していればnumber型であり、そうでなければstring型になります
  • したがってCat1はnumber型になります、228 extends numberは真なので
  • Cat2はstring型になります、'mike' extends numberは偽なので
type CatNumber<T> = T extends number ? number : string;

type Cat1 =  CatNumber<228>;
type Cat2 =  CatNumber<'mike'>;

条件型を組み合わせて、さらに複雑な型も作れます。

type CatTypes<T> = T extends number
  ? number
  : T extends boolean
  ? boolean
  : T extends null
  ? null
  : string;

type Cat11 = CatTypes<228>;     // → number
type Cat12 = CatTypes<'mike'>;  // → string
type Cat13 = CatTypes<true>;    // → boolean
type Cat14 = CatTypes<null>;    // → null

infer型演算子

さて、本題のinferですが、inferは日本語では推論するです。

ここでも簡単な例で説明します、

  • ArrayElem型は型パラメーターTに渡された配列の要素の型になります
    • Uはinferが推論した型になり、?以降の値(式)で利用できます
  • number[]ならnumberstring[]ならstring
  • numberが渡されるとneverになります、numberは配列ではないからです
type ArrayElem<T> = T extends Array<infer U> ? U : never;

type A1 = ArrayElem<number[]>;    // → number
type A2 = ArrayElem<string[]>;    // → string
type A3 = ArrayElem<number>;      // → never

もう1つ、例を出してみます。

  • Tuple2は、2つの型パラメーターで指定された2要素の配列(タプル)型です
  • NameAgeTuple2を使い、[文字列, 数]の配列(タプル)型です
  • FirstTypeTuple2の最初の要素の型、このようにinferが活躍します
  • SecondTypeTuple2の2番目の要素の型
  • FirstType<NameAge>SecondType<NameAge> でNameAge型配列の最初の要素、2番目の要素の型が取得できます
type Tuple2<A, B> = [A, B];
type NameAge = Tuple2<string, number>;
type FirstType<T> = T extends Tuple2<infer F, infer S> ? F : never;
type SecondType<T> = T extends Tuple2<infer F, infer S> ? S : never;

const cat1: NameAge = ["Mike", 4];
const cat1name: FirstType<NameAge> = cat1[0];   // const cat1name: string = cat1[0]
const cat1age: SecondType<NameAge> = cat1[1];   // const cat1age: number  = cat1[1]

このように、型パラメーターで構築された複雑な型から型の一部を取り出せ、便利に型を扱えます。

Generated<T>の解説

これからGenerated<T>で使われているColumnType型の定義は以下です、オブジェクト型ですね。
要素の名前は__select____insert____update__で、各要素の型は型パラメーター SelectTypeInsertType(省略時はSelectType)、UpdateType(省略時はSelectType)で指定されます。

export type ColumnType<SelectType, InsertType = SelectType, UpdateType
                       = SelectType> = {
    readonly __select__: SelectType;
    readonly __insert__: InsertType;
    readonly __update__: UpdateType;

さて、Generated<T>の定義は、

export type Generated<T> = T extends ColumnType<infer S, infer I, infer U>
  ? ColumnType<S, I | undefined, U>
  : ColumnType<T, T | undefined, T>;

TがColumnTypeを継承した型かチェックし

  • そうならは、ColumnType<S, I | undefined, U>の型になります
    • InsertTypeはundefinedでも良くなっています
  • ちがえば、ColumnType<T, T | undefined, T>になります
    • 同様に、InsertTypeはundefinedでも良くなっています

実際にGenerated<T>を使っている部分を見てみましょう。

Generated<number>

id: Generated<number>; の場合、numberはColumnTypeを継承してないので以下のようになります。

id: ColumnType<number, number | undefined, number>;

Generated<Timestamp>

前回のブログでは省略した部分に以下のようなコードがありました。

export type Timestamp = ColumnType<Date, Date | string>;
・・・
started_at: Generated<Timestamp>;

TimestampColumnTypeを継承しているので、以下のようになります。

started_at: ColumnType<Date, Date | string | undefined, Date>;

前回のブログで説明したようにKyselyでは、検索結果の型、INSERT文のデータ型、UPDATE文のデータ型が自動的に作られるのですが、これらの型の元になるのがColumnTypeです。

まとめ

今回infer型演算子条件型(Conditional Types) を学んだので、TypeScriptの複雑な型への理解が少し進みました。

複雑な型を作るための型演算子と、条件型のようなパターンを少しずつ学んで行く必要がありますね。inferで指定する型変数(型パラメーター)は出力というかバインド(Bind)なんですね。

型への旅はまだまだ続きます・・・

- about -

EY-Office代表取締役
・プログラマー
吉田裕美の
開発者向けブログ