型パズル難しい

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


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日が溶けた。型パズルムズイ。