简体   繁体   中英

Get typescript to infer tuple parameters types

I'm creating a function like Lodash's at() . I have typing working if the user passes in tuples like this:

at(obj, ['key1'] as const, ['key2', 'key3'] as const)

I want the user to be able to call the function naturally, without tricks like " as const ". Can it be done? Here is the craziness I have so far:

type PropertyAtPath<T, Path extends readonly any[]> = Path extends []
  ? T
  : Path extends readonly [infer First, ...infer Rest]
  ? First extends keyof T
    ? PropertyAtPath<T[First], Rest>
    : undefined
  : unknown;

type At<T, Paths extends ReadonlyArray<ReadonlyArray<any>>> = {
  [I in keyof Paths]: Paths[I] extends readonly any[]
    ? PropertyAtPath<T, Paths[I]>
    : never;
};

declare function at<T, Paths extends ReadonlyArray<ReadonlyArray<any>>>(
  object: T,
  ...paths: Paths
): At<T, Paths>;

playground link

First of all, you need to validate second argument, to avoid passing invalid object paths.

Here , here , here and here, in my blog you can find an explanation of how this code works.

First three links are from stackoverflow. I have provided explanation in comments.

type Structure = {
    foo: {
        a: [1, 'hello'],
        b: 2,
    }
    bar: {
        c: 3,
        d: 4,
    }
}

declare var data: Structure;

type Values<T> = T[keyof T]

type Elem = string;

type Acc = Record<string, any>

/**
 * Just like Array.prototype.reduce predicate/callback
 * Receives accumulator and current element
 * - if element extends one of accumulators keys -> return  acc[elem]
 * - otherwise return accumulator
 */
type Callback<Accumulator extends Acc, El extends Elem> =
    El extends keyof Accumulator ? Accumulator[El] : Accumulator

type Reducer<
    Keys extends Elem,
    Accumulator extends Acc = {}
    > =
    /**
     * If Keys extends a string with dot
     */
    Keys extends `${infer Prop}.${infer Rest}`
    /**
     * - Call Reducer recursively with last property
     */
    ? Reducer<Rest, Callback<Accumulator, Prop>>
    /**
     *  - Otherwise obtain whole property 
     */
    : Keys extends `${infer Last}`
    ? Callback<Accumulator, Last>
    : never
{

    type Test1 = Reducer<'foo.a', Structure> // 1
    type Test2 = Reducer<'bar.d', Structure> // 4
}

/**
 * Compute all possible property combinations
 */
type KeysUnion<T, Cache extends string = ''> =
    /**
     * If T extends string | number | symbol -> return Cache, this is the end
     */
    T extends PropertyKey ? Cache : {
        /**
         * Otherwise, iterate through keys of T, because T is an object
         */
        [P in keyof T]:
        /**
         * Check if property extends string
         */
        P extends string
        /**
         * Check if it is the first call of this utility,
         * because Cache is empty
         */
        ? Cache extends ''
        /**
         * If it is a first call,
         * call recursively itself, go one level down - T[P] and initialize Cache - `${P}`
         */
        ? KeysUnion<T[P], `${P}`>
        /**
         * If it is not first call of KeysUnion and not the last
         * Unionize Cache with recursive call, go one level dow and update Cache
         */
        : Cache | KeysUnion<T[P], `${Cache}.${P}`>
        : never
    }[keyof T]

{
    //"foo" | "bar" | "foo.a" | "foo.b" | "bar.c" | "bar.d"
    type Test1 = KeysUnion<Structure>
}

type ExtractPath<T extends string> = Extract<T, string>

type Mapper<Obj, Paths extends ExtractPath<KeysUnion<Obj>>[]> = {
    [Prop in keyof Paths]: Reducer<Paths[Prop] & string, Obj>
}

const at = <
    Obj,
    Key extends ExtractPath<KeysUnion<Obj>> & string,
    Keys extends Key[]
>(obj: Obj, keys: [...Keys]): Mapper<Obj, Keys> =>
    null as any


type Result = Mapper<Structure, ['foo.a.1', 'bar.c']>

const lookup = at(data, ['foo.a.0', 'bar.c']) // [1, 3]

Playground

Above code expects obj to be fully infered, I mean obj should be as const .

If you want to handle arrays and empty tuples you might want to use this implementation:



type Values<T> = T[keyof T]
{
    // 1 | "John"
    type _ = Values<{ age: 1, name: 'John' }>
}

type IsNever<T> = [T] extends [never] ? true : false;
{
    type _ = IsNever<never> // true 
    type __ = IsNever<true> // false
}

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 IsEmptyTuple<T extends Array<any>> = T['length'] extends 0 ? true : false
{
    type _ = IsEmptyTuple<[]> // true
    type __ = IsEmptyTuple<[1]> // false
    type ___ = IsEmptyTuple<number[]> // false
}

/**
 * If Cache is empty return Prop without dot,
 * to avoid ".user"
 */
type Concat<
    Cache extends PropertyKey[],
    Prop extends string | number | symbol
    > =
    Cache extends []
    ? [Prop]
    : [...Cache, Prop]

/**
 * Simple iteration through object properties
 */
type HandleObject<Obj, Cache extends PropertyKey[]> = {
    [Prop in keyof Obj]:
    | Cache
    // concat previous Cacha and Prop
    | Concat<Cache, Prop>
    // with next Cache and Prop
    | Path<Obj[Prop], Concat<Cache, Prop>>
}[keyof Obj]

type Path<Obj, Cache extends PropertyKey[] = []> =
    (Obj extends PropertyKey
        // return Cache
        ? Cache
        // if Obj is Array (can be array, tuple, empty tuple)
        : (Obj extends Array<any>
            // and is tuple
            ? (IsTuple<Obj> extends true
                // and tuple is empty
                ? (IsEmptyTuple<Obj> extends true
                    // call recursively Path with `-1` as an allowed index
                    ? Path<PropertyKey, Concat<Cache, -1>>
                    // if tuple is not empty we can handle it as regular object
                    : HandleObject<Obj, Cache>)
                // if Obj is regular  array call Path with union of all elements
                : Path<Obj[number], [...Cache, `${number}`]>
            )
            // if Obj is neither Array nor Tuple nor Primitive - treat is as object    
            : HandleObject<Obj, Cache>
        )
    )


type Reducer<Obj, Props extends Array<PropertyKey>> =
    Props extends []
    ? Obj
    : (Props extends [infer Fst, ...infer Tail]
        ? (Tail extends string[]
            ? (
                Obj extends Array<any>
                ? (
                    Fst extends `${number}`
                    ? Reducer<Obj[number], Tail>
                    : never)
                : (Fst extends keyof Obj
                    ? Reducer<Obj[Fst], Tail>
                    : never
                )
            )
            : never
        )
        : never
    )

type Validation<T> = IsNever<T> extends true ? [never] : []

const at = <
    Obj,
    Keys extends string[]
>(obj: Obj, keys: [...Keys], ...validation: Validation<Reducer<Obj, Keys>>): Reducer<Obj, [...Keys]> =>
    null as any


type Structure = {
    empty: [],
    tuple: [1, 2, 3],
    array: { age: Array<{ surname: string }> }[]
}


declare const data: Structure

/**
 * Tests
 */

// [1, 2, 3]
const _ = at(data, ['tuple'])

// const __: {
//     age: Array<{
//         surname: string;
//     }>;
// }
const __ = at(data, ['array', '0'])

// const ___: {
//     surname: string;
// }[]
const ___ = at(data, ['array', '2', 'age'])

// const ____: {
//     surname: string;
// }
const ____ = at(data, ['array', '0', 'age', '2'])

/**
 * Expected never
 */
{
    const _ = at(data, ['tupl'],) // error
    const __ = at(data, ['array', 'w']) // error
}

Playground

In case someone stumbles upon this in the future.

If you just want to have the function infer a tuple instead of an array without using as const , there are multiple options.


The preferred option is to use a variadic tuple type :

function fn<T extends string[]>(tuple: [...T]) { return tuple }

const r = fn(["a", "b", "c"])
//    ^? const r: ["a", "b", "c"]

There is also this alternative:

function fn<T extends string[] | [string]>(tuple: T) { return tuple }

This behaves identical to the first example in most cases. However I encountered some differences in the past where the second solution works but the first one does not .


Playground

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM