🥹

TypeScriptは26以上のメンバーを絞り込めない!

2024/06/04に公開

この記事で扱うコードは全てTypeScript Playgroundにまとめていますので、
もし実際にコードを確認したい場合はそちらでご確認ください。

26以上のメンバーを持つMappedTypeで型の絞り込みができない

表題の通りですが、実際にコードを見てもらうのがわかりやすいでしょう。

const maxMap = {
    one: {a: 1},
    two: {b: 2},
    three: {c: 3},
    four: {d: 4},
    ...
    twentyThree: {w:23},
    twentyFour: {x:24},
    twentyFive: {y:25},
    twentySix: {z:26},
}

このように26のkeyを持つオブジェクトがあるとします。
これを元に、key,valueの組み合わせが決まっているMapped Typeを作成します。

type MapKey = keyof MaxMap
type MapValue<T extends MapKey> = MaxMap[T]

interface ObjectImpl<Key, Value> {
  key: Key;
  value: Value;
}

type ObjectTypesByName = {
    [K in MapKey]: ObjectImpl<K, MaxMap[K]>
}

type MaxObject<T extends MapKey = MapKey> = ObjectTypesByName[T]

MaxObjectは下記のようなデータ構造になります。

const maxObject = {
    key: 'one',
    value: {a: 1}
}

MaxObjectはkeyとvalueの組み合わせが予め定義されているため、keyが決まると自然とvalueの型も決まります。
これを利用した記述を書いてみましょう。

function checkMap(maxKey:MapKey) {
    const b:MaxObject = {
        key: maxKey,
        value: null as any
    }
    if (b.key === 'twentySix') {
        console.log(b.value.z)
    }
}

valueをanyとしており、b.key === 'twentySix'によってvalueがanyから絞り込まれるはずです。
ですが...

実際にはこの通り、代入時点で型推論に失敗しています。
ちなみに、メンバーの数を25以下にすると成功します。

type MaxMap = {
    one: {a: 1},
    ...
    twentyFive: {y:25},
    // twentySix: {z:26}, ←ここをコメントアウト
}

...
function checkMap(maxKey:MapKey) {
    const b:MaxObject = { // 通る
        key: maxKey,
        value: null as any
    }
    ...
}

この現象はissueがあるのですが、現在はCloseされています。
タイトルは「Limit of 25 members when using union of mapped type」でまんまですね。
https://github.com/microsoft/TypeScript/issues/40803

we try to make this particular kind of assignment work anyway by enumerating all possible values that the initializer could have, and seeing if that value would be assignable to AWSObject. There's a hardcoded limit here of 24 candidates (more typically hit when you have e.g. 2 x 3 x 3 possible values) for this process to avoid combinatorial explosions.

ChatGPTを元に翻訳

私たちはこの特定の種類の割り当てを機能させるために、初期化子が持つ可能性のあるすべての値を列挙し、その値が対象のオブジェクトに割り当て可能かどうかを確認することで試みます。このプロセスで組み合わせ爆発を避けるために、候補の数には24のハードコードされた制限があります(通常は例えば2 x 3 x 3のような場合に達します)。

この仕様は組み合わせ爆発を防ぐためであり、候補の掛け合わせが24までだとコメントがあります。
(実際に確認できる制約は25までのようですが、このコメントでは24までと記載されているのが謎です。何かご存知の方がいればぜひ教えてください!)

タグ付きUnionで挙動を確認する

試しにタグ付きUnionで同じ処理を実行してみましょう。

type TaggedUnion = 
    {type: 'one', a?: 1} 
    | {type: 'two', b?: 2} 
    | {type: 'three', c?: 3}
    | {type: 'four', b?: 4}
    ...
    | {type: 'twentySix', z?:26}

Mapped Typeの時と同じく、特定のUnionに絞り込みができない状態の型を持つ変数を用意します。

type TaggedKey = TaggedUnion['type']

function checkTaggedUnion(name:TaggedKey) {
    const taggedItem: TaggedUnion = {
        type: name
    }
    if(taggedItem.type === 'twentyFive') {
        console.log(taggedItem.y)
    }
}

すると、Mapped Typeと同じように以下の型エラーが出現します。

どうすればいいの?

const taggedItem: TaggedUnion = {
    type: name
}

のような型の広い変数からの絞り込み、という記述を回避するしか今のところはなさそうです。
タグ付きユニオンであれば、下記のような単なるタグによる絞り込みは実行可能です。

function checkTaggedUnion2(taggedUnion: TaggedUnion) {
    if(taggedUnion.type === 'twentySix') {
        console.log(taggedUnion.z)
    }

MappedTypeであれば下記の記述が有効です。

function checkMap2(maxObject:MaxObject) {
    if(maxObject.key === 'twentySix') {
        console.log(maxObject.value.z)
    }
}
サイボウズ フロントエンド

Discussion