2

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 Answer 1

1

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.

Sign up to request clarification or add additional context in comments.

1 Comment

Thank you for the swift and detailed reply, but your code seems not including the key feature I was looking for, that is, the pair should be able to be used as an accessor of a value of some certain type T. For example, if const pair: AccessorPair<number>, I can expect the type of pair[0][pair[1]] to be inferred as number. Do you have any opinion on such type definition?

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.