TypeScript: Generisk typ av objekt-egenskapstupel av viss värdetyp


Jag undrar om det är möjligt att definiera en generisk typ som är en tuppel med längd 2 [O, K], kalla den AccessorPair, där den första typen O är ett objekt och den andra typen K är en nyckel till det objektet, så att O[K] utvidgar en viss typ T.

Till exempel,

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

Mitt första försök är

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

men det leder bara till "Typ 'K' kan inte användas för att indexera typ 'O'.ts(2536)"

Jag försökte arbeta runt genom att

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

och det löses alltid till "aldrig".

Är en sådan definition för vag för att stödjas av TypeScript? Finns det något programmeringsspråkskoncept jag inte känner till när det gäller sådan typning? All diskussion uppskattas!

Svar #1

Först och främst behöver vi skilja mellan två definitioner: arrays och tuples i TypeScript.

Tänk på array som en lista av okänd längd, något som vector i Rust.

Tänk på tuple som en lista med känd längd under kompileringen.

För att hantera tillåtna nycklar för [true, false]-tuplen och det bara finns två av dem 0 | 1 behöver vi skriva en liten hjälptyp:

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

Nu behöver vi ett verktyg som villkorsvis returnerar tillåtna nycklar för tuplar och arrayer:

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

ComputeKeys returnerar 1|0 för [true, false] och alla array-nycklar för vanlig array.

Förresten, vill du tillåta att använda forEach, reduce, map-nycklar som ett andra element i tuplen?

Vi behöver också hantera alla icke-arrayobjekt. Låt oss skapa typen Json med alla serialiserbara typer i en union:

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

Lekplats

Förresten, eftersom du inte känner till alla tillåtna objekt i en tuppel, är det omöjligt att göra det utan en extra funktion.