型パズル難しい

あるテーブルへの操作について考える。

1操作をOp型で表す

type Op<T> = {
    table: string
    pk: (keyof T)[]
    cols: Partial<T>
}

keyof Tは型Tが持つプロパティ名のみの型になる。

type MyObj = {
    prop1: number
    prop2: string
}
type MyObjKeys = keyof MyObj // == 'prop1' | 'prop2'

Partial<T>は型Tのすべてプロパティをundefined可能にする

const obj1: MyObj = {prop1:0} // error porp2は必須

type MyObjPartial = Partial<MyObj> // == { prop1?:number, prop2?:string }
const obj2: MyObjPartial = {prop1:0} // OK

これらの組み合わせで型Op<T>は、pkには必ずTに存在するプロパティ名、colsには必ずTの部分型を引数として渡すことを制約できる。

const op1: Op<MyObj> = { table: 'myobjs', pk:['prop1'], { prop1: 1, prop2: ''} }
const op2: Op<MyObj> = { table: 'myobjs', pk:['prop3'], { prop1: 1, prop2: ''} } // error
const op3: Op<MyObj> = { table: 'myobjs', pk:['prop1'], { prop1: 1, prop3: ''} } // error 

ところで、JavaScriptの配列には、違う方のものを詰め込める。なのでこんな関数も作れる

function batch(operations) {
    // 同一トランザクションでoperationsすべてを実行する
}

batch([
    { table: 'A', pk: ['id'], { id: 1, col1: '' } },
    { table: 'B', pk: ['code'], { code: 'c', col1: '' } }
])

これに型を付けたいのが本題。

普通に型を付けてみる。

function batch<T>(operations: Op<T>) {
}

ダメだというの容易に想像がつく。これでは、すべてのOpの型パラメータTが一緒でないと通らない。
それは思っているやつじゃない。

ググっていたらinferにたどり着いた。

extendsとこのinferを使用することで型を計算することができるという。これが結構難しく巷では型パズルと呼ばれるらしい。

次のToArrayextendsinferを使用して、配列の各要素の型を取り出す式だ。
stringnumberからなる要素数2の配列(タプル)があったとすると、(string|number)[][string, number]もしくは[number, string]に推論してくれる。

type ToArray<T> = T extends [] ? [] 
                : T extends [infer U, ...infer V] ? [U, ...ToArray<V>]
                : T

function f<T extends unknown[]>(...args: ToArray<T>): ToArray<T> {
    return args;
}

f(0, 'a') // これの戻り値の型は [number, string]になる。
f('a', 0) // これの戻り値の型は [string, number]になる。

extendsは三項演算子を伴って、型を導出する式をかける。
少し難しいのは T extends [infer U, ...infer V] ? [U, ...ToArray<V>]の部分。T extends [infer U, ...infer V]の部分はTが要素を持つ配列だった時、先頭の要素をU、それ以降の要素をまとめてVとして当てはめて推論させている。これにマッチした時、[U, ...ToArray<V>]で、先頭の要素の型Uは確定して、残りの要素Vを再帰的にマッチングさせる。その結果を...でフラットに展開させている。

もし、[U, ...ToArray<V>][U,ToArray<V>]と書くと結果は[string,[number]]とLispみたいにネストしていく。

このToArrayを調整し、Opを要素に持つ、BatchOp<T>を書いてみた。

type Op<T> = {
    table: string
    pk: (keyof T)[]
    cols: Partial<T>
}

type BatchOp<T> = T extends [Op<infer U>, ...infer V] ? [Op<U>, ...BatchOp<V>] : []

function batch<T>(ops: BatchOp<T>) {
  // transaction
}

type V1 = {
    id: number
    col1: string
}

type V2 = {
    code: string
    col2: string
}

const v1 = { id: 1, col1: 'col1' }
const v2 = { code: 'code', col2: 'col2' }

batch<[Op<V1>,Op<V2>,Op<V2>]>([
    { table: 'A', pk: ['id'], cols: v1 },
    { table: 'B', pk: ['code'], cols: v2 },
    { table: 'B', pk: ['id'], value: v2 }, // 無事エラーになる
])

狙った通りの要素でエラーを出せた。

ここにたどり着くまで1日が溶けた。型パズルムズイ。


You'll only receive email when they publish something new.

More from iwate
All posts