ウェブサイト検索

TypeScript: 特定の値の型を持つオブジェクトプロパティタプルのジェネリック型


長さ2のタプル [O, K] をジェネリック型として定義できるかどうか疑問に思っています。それを AccessorPair と呼び、最初の型 O はオブジェクト、2番目の型 K はそのオブジェクトのキーであり、O[K] が特定の型 T を拡張するようにすることができますか。

例えば、

const foo: AccessorPair<number> = [new Array(), 'length']
const bar: AccessorPair<boolean> = [[true, false], 0]

私の最初の試みは

type AccessorPair<T> = T extends (infer O)[infer K] ? [O, K] : never

しかし、それは単に「タイプ 'K' はタイプ 'O' のインデックスとして使用できません。ts(2536)」につながるだけです

私は~で回避しようとした

type ValueOf<O, P extends keyof O> = O[P]
type AccessorPair<T> = T extends ValueOf<infer O, infer P> ? [O, P] : never

そして、それは常に「決して」に解決されます。

そのような定義はTypeScriptでサポートされるには曖昧すぎますか?そのような型付けに関して、私が知らないプログラミング言語の概念はありますか?どんな議論でも歓迎です!

回答 #1

まず第一に、2つの定義を区別する必要があります:TypeScriptのarraysとtuplesです。

長さが不明なリストとして array を考えてみてください。Rust の vector のようなものです。

tupleを、コンパイル時に長さが既知のリストと考えてください。

[true, false] タプルの許可されたキーを扱うため、そしてそれが 0 | 1 の二つしかない場合、小さなユーティリティ型を書く必要があります:

type IsTuple<T> =
    (T extends Array<any> ?
        (T['length'] extends number
            ? (number extends T['length']
                ? false
                : true)
            : true)
        : false)

{
    type _ = IsTuple<[1, 2]> // true
    type __ = IsTuple<number[]> // false
    type ___ = IsTuple<{ length: 2 }> // false
}

type AllowedTupleLength<
    T extends ReadonlyArray<unknown>,
    Length extends number = never
    > = T extends readonly [infer _, ...infer Tail]
    ? AllowedTupleLength<Tail, Length | Tail['length']>
    : T extends readonly []
    ? Length
    : never;

type Result = AllowedTupleLength<[0,0]> // 1 | 0

次に、タプルと配列の許可されたキーを条件付きで返すユーティリティが必要です:

type ComputeKeys<Tuple extends any[]> =
    IsTuple<Tuple[0]> extends true ? AllowedTupleLength<Tuple[0]> : keyof Tuple[0]

`ComputeKeys` は `[true, false]` に対して `1|0` を返し、通常の配列に対しては全ての配列キーを返します。

ところで、タプルの2番目の要素として forEach, reduce, map キーの使用を許可しますか?

また、すべての非配列オブジェクトも扱う必要があります。すべてのシリアライズ可能な型をユニオンにした Json 型を作成しましょう:

type Json = | string | number | boolean | { [prop: string]: Json } | Array<Json>

type IsTuple<T> =
    (T extends Array<any> ?
        (T['length'] extends number
            ? (number extends T['length']
                ? false
                : true)
            : true)
        : false)

type AllowedTupleLength<
    T extends ReadonlyArray<unknown>,
    Length extends number = never
    > = T extends readonly [infer _, ...infer Tail]
    ? AllowedTupleLength<Tail, Length | Tail['length']>
    : T extends readonly []
    ? Length
    : never;

type ComputeKeys<Tuple extends any[]> =
    IsTuple<Tuple[0]> extends true ? AllowedTupleLength<Tuple[0]> : keyof Tuple[0]


function handleTuple<
    Elem extends Exclude<Json, any[]>, // overload signature for non array serializable values
    Tuple extends Elem[]
>(tuple: [...Tuple, keyof Tuple[0]]): [...Tuple]
function handleTuple<
    Elem,
    NestedTuple extends Elem[],
    Tuple extends [...NestedTuple][]
>(tuple: [...Tuple, ComputeKeys<Tuple>]): [...Tuple]
function handleTuple(tuple: unknown[]) {
    return tuple
}

handleTuple([[true, false], 1]) // ok
handleTuple([[true, false], 2]) // expected error, index is too big

handleTuple([new Array(), 'length']) // ok
handleTuple([new Array(), 2]) // allowed because we don't know exact length of array
handleTuple([{ name: 'John', age: 32 }, 'name']) // allowed because we don't know exact length of array

遊び場

ちなみに、タプルの中で許可されているすべてのオブジェクトを知らないので、追加の関数なしで作ることは不可能です。