TypeScript: Generic type of object-property tuple of certain value type

Viewed 566

I'm wondering whether it's possible to define a generic type that is a length-2 tuple [O, K], call it AccessorPair, whose first type O is an object and second type K is a key to that object, such that O[K] extends a certain type T.

For example,

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

My first attempt is

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

but it just leads to "Type 'K' cannot be used to index type 'O'.ts(2536)"

I tried to work around by

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

and it always resolves to "never".

Is such definition too vague to be supported by the TypeScript? Is there any programming language concept I don't know concerning such typing? Any discussion is appreciated!

1 Answers

First of all we need to distinguish two definitions: arrays and tuples in typescript.

Consider array as a list of unknown length, something like vector in Rust.

Consider tuple as a list of known, during the compilation, length.

In order to handle allowed keys for [true, false] tuple and there are only two of them 0 | 1 we need to write a small utility type:

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

Now we need an utility which will conditionaly return allowed keys for tuple and array:

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

ComputeKeys returns 1|0 for [true, false] and all array keys for regular array.

Btw, do you want to allow using forEach, reduce, map keys as a second element in the tuple ?

Also, we need to handle all non array objects. Let's create Json type with all serializable types in a 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

Playground

Btw, since you don't know all allowed objects in a tuple, it is impossible to make it without extra function.

Related