Note: The Listed platform will permanently shut down December 31, 2026. Your data published on Listed will still be available in your personal Standard Notes account. Learn more

型パズル難しい

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

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


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

More from iwate
All posts