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.
