型パズル難しい
October 7, 2022•335 words
あるテーブルへの操作について考える。
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
を使用することで型を計算することができるという。これが結構難しく巷では型パズルと呼ばれるらしい。
次のToArray
はextends
とinfer
を使用して、配列の各要素の型を取り出す式だ。
string
とnumber
からなる要素数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日が溶けた。型パズルムズイ。