是否可以纯粹使用 TypeScript 为对象创建对象键路径字符串自动完成?

Is it possible to create object key path string autocomplete for an object purely with TypeScript?

const val = {
    a: {
        b: {
            c: 1
        }
    }
};

我有很多不同结构的对象。我有一组函数需要遍历对象中的数据。我想利用 TypeScript 来帮助我确保输入是整个过程中的关键。

像这样都可以:

myFn('a.b.c'); 
myFn('a', 'b', 'c'); 

如果用户类型:

myFn('a.x.c'); 
myFn('a', 'b', 'x'); 

这些应该作为错误突出显示。

可能吗?

关于第一个例子,用dot表示法,你可以用这个例子:

type Foo = {
    user: {
        description: {
            name: string;
            surname: string;
        }
    }
}

declare var foo: Foo;

/**
 * Common utils
 */

type Primitives = string | number | symbol;

/**
 * Obtain a union of all values of object
 */
type Values<T> = T[keyof T]

type Elem = string;

type Acc = Record<string, any>

/**
 * Custom user defined typeguard
 */
const hasProperty = <Obj, Prop extends Primitives>(obj: Obj, prop: Prop)
    : obj is Obj & Record<Prop, any> =>
    Object.prototype.hasOwnProperty.call(obj, prop);

/**
 * Obtain value by object property name
 */
type Predicate<Accumulator extends Acc, El extends Elem> =
    El extends keyof Accumulator ? Accumulator[El] : Accumulator

/**
 * If first argument is empty string, avoid using dot (.)
 * If first argument is non empty string concat two arguments andput dot (.)
 * between them
 */
type Concat<Fst, Scd> =
    Fst extends string
    ? Scd extends string
    ? Fst extends ''
    ? `${Scd}`
    : `${Fst}.${Scd}`
    : never
    : never
{
    type Test = Concat<'hello','bye'> // "hello.bye"
}    

/**
 * Obtain union of all possible paths
 */
type KeysUnion<T, Cache extends string = ''> =
    T extends Primitives ? Cache : {
        [P in keyof T]:
        | Concat<Cache, P>
        | KeysUnion<T[P], Concat<Cache, P>>
    }[keyof T]

{
    // "user" | "user.description" | "user.description.name" | "user.description.surname"
    type Test = KeysUnion<Foo>
}

/**
 * Get object value by path
 */
type GetValueByPath<T extends string, Cache extends Acc = {}> =
    T extends `${infer Head}.${infer Tail}`
    ? GetValueByPath<Tail, Predicate<Cache, Head>>
    : Predicate<Cache, T>
{
    // {
    //   name: string;
    //   surname: string;
    // }
    type Test = GetValueByPath<"user.description", Foo>
}

/**
 * Obtain union of all possible values, 
 * including nested values
 */
type ValuesUnion<T, Cache = T> =
    T extends Primitives ? T : Values<{
        [P in keyof T]:
        | Cache | T[P]
        | ValuesUnion<T[P], Cache | T[P]>
    }>

/**
 * Function overloading
 */
function deepPickFinal<Obj, Keys extends KeysUnion<Obj>>
    (obj: ValuesUnion<Obj>, keys: Keys): GetValueByPath<Keys, Obj>

function deepPickFinal<Obj, Keys extends KeysUnion<Obj> & Array<string>>
    (obj: ValuesUnion<Obj>, ...keys: Keys) {
    return keys
        .reduce(
            (acc, elem) => hasProperty(acc, elem) ? acc[elem] : acc,
            obj
        )
}

/**
 * Ok
 */
const result = deepPickFinal(foo, 'user.description') // ok { name: string; surname: string; }
const result2 = deepPickFinal(foo, 'user') // ok

Playground

您可能已经注意到,return 类型也会被推断出来。 完整的描述和解释,您可以在 my article.

中找到

如果你想使用剩余参数,comma separated,请参考我的文章