普通にTypeScriptを使ってReactアプリを書いているだけなら複雑な型宣言を目にする事は少ないです。しかしReactや他のライブラリーの型宣言には複雑なものがありますね。
私も複雑な型宣言は得意ではなく、複雑な型宣言が表示されるとそっと閉じてしまう事もままあります。😅
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
が推論した型になり、?
以降の値(式)で利用できます
- Uは
number[]
ならnumber
、string[]
なら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要素の配列(タプル)型ですNameAge
はTuple2
を使い、[文字列, 数]
の配列(タプル)型ですFirstType
はTuple2
の最初の要素の型、このようにinfer
が活躍しますSecondType
はTuple2
の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__
で、各要素の型は型パラメーター SelectType
、 InsertType
(省略時は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
でも良くなっています
- InsertTypeは
- ちがえば、
ColumnType<T, T | undefined, T>
になります- 同様に、InsertTypeは
undefined
でも良くなっています
- 同様に、InsertTypeは
実際に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>;
Timestamp
はColumnType
を継承しているので、以下のようになります。
started_at: ColumnType<Date, Date | string | undefined, Date>;
前回のブログで説明したようにKyselyでは、検索結果の型、INSERT文のデータ型、UPDATE文のデータ型が自動的に作られるのですが、これらの型の元になるのがColumnType
です。
まとめ
今回infer型演算子 と 条件型(Conditional Types) を学んだので、TypeScriptの複雑な型への理解が少し進みました。
複雑な型を作るための型演算子と、条件型のようなパターンを少しずつ学んで行く必要がありますね。inferで指定する型変数(型パラメーター)は出力というかバインド(Bind)なんですね。
型への旅はまだまだ続きます・・・