让我们分几个步骤来做。
// Simple union type for primitives
type Primitives = string | number | symbol;
keys 的类型应该允许按严格顺序排列的道具,并且它应该是一个数组(因为 rest 运算符)。
我认为这里最好的方法是创建一个包含所有可能参数的联合类型。
让我们开始吧。
type NestedKeys<T, Cache extends Array<Primitives> = []> = T extends Primitives ? Cache : {
[P in keyof T]: [...Cache, P] | NestedKeys<T[P], [...Cache, P]>
}[keyof T]
// ["test1"] | ["test2"] | ["test2", "test2Nested"] | ["test2", "test2Nested", "something"] | ["test2", "test2Nested", "somethingElse"] | ["test2", "test2Nested", "test3Nestend"] .....
现在,我们应该为我们的 reducer 逻辑编写一个类型。
type Elem = string;
type Predicate<Result extends Record<string, any>, T extends Elem> = T extends keyof Result ? Result[T] : never
type Reducer<
Keys extends ReadonlyArray<Elem>,
Accumulator extends Record<string, any> = {}
> = Keys extends []
? Accumulator
: Keys extends [infer H]
? H extends Elem
? Predicate<Accumulator, H>
: never
: Keys extends readonly [infer H, ...infer Tail]
? Tail extends ReadonlyArray<Elem>
? H extends Elem
? Reducer<Tail, Predicate<Accumulator, H>>
: never
: never
: never;
这种类型几乎和你在 reducer 中所做的完全一样。为什么差不多?因为它是递归类型。
我为变量提供了相同的名称,因此更容易理解这里发生的事情。
更多示例您可以在我的博客中找到here。
创建完所有类型后,我们可以通过测试来实现函数:
const getByPath = <Obj, Keys extends NestedKeys<Obj> & string[]>(obj: Obj, ...keys: Keys): Reducer<Keys, Obj> =>
keys.reduce((acc, elem) => acc[elem], obj as any)
getByPath(test, 'test1') // ok
getByPath(test, 'test1', 'test2Nested') // expected error
getByPath(test, 'test2') // ok
const result = getByPath(test, 'test2', 'test2Nested') // ok -> { something: string; somethingElse: string; test3Nestend: { end: string; }; }
const result3 = getByPath(test, 'test2', 'test2Nested', 'test3Nestend') // ok -> {end: stirng}
getByPath(test, 'test2', 'test2Nested', 'test3Nestend', 'test2Nested') // expeted error
const result2=getByPath(test, 'test2', 'test2Nested', 'test3Nestend', 'end') // ok -> string
getByPath(test, 'test2', 'test2Nested', 'test3Nestend', 'end', 'test2') // expected error
Playground
More exaplanation You can find in my blog
点符号
type Foo = {
user: {
description: {
name: string;
surname: string;
}
}
}
declare var foo: Foo;
type Primitives = string | number | symbol;
type Values<T> = T[keyof T]
type Elem = string;
type Acc = Record<string, any>
// (acc, elem) => hasProperty(acc, elem) ? acc[elem] : acc
type Predicate<Accumulator extends Acc, El extends Elem> =
El extends keyof Accumulator ? Accumulator[El] : Accumulator
type Reducer<
Keys extends Elem,
Accumulator extends Acc = {}
> =
Keys extends `${infer Prop}.${infer Rest}`
? Reducer<Rest, Predicate<Accumulator, Prop>>
: Keys extends `${infer Last}`
? Predicate<Accumulator, Last>
: never
const hasProperty = <Obj, Prop extends Primitives>(obj: Obj, prop: Prop)
: obj is Obj & Record<Prop, any> =>
Object.prototype.hasOwnProperty.call(obj, prop);
type KeysUnion<T, Cache extends string = ''> =
T extends Primitives ? Cache : {
[P in keyof T]:
P extends string
? Cache extends ''
? KeysUnion<T[P], `${P}`>
: Cache | KeysUnion<T[P], `${Cache}.${P}`>
: never
}[keyof T]
type O = KeysUnion<Foo>
type ValuesUnion<T, Cache = T> =
T extends Primitives ? T : Values<{
[P in keyof T]:
| Cache | T[P]
| ValuesUnion<T[P], Cache | T[P]>
}>
declare function deepPickFinal<Obj, Keys extends KeysUnion<Obj>>
(obj: ValuesUnion<Obj>, keys: Keys): Reducer<Keys, Obj>
/**
* Ok
*/
const result = deepPickFinal(foo, 'user') // ok
const result2 = deepPickFinal(foo, 'user.description') // ok
const result3 = deepPickFinal(foo, 'user.description.name') // ok
const result4 = deepPickFinal(foo, 'user.description.surname') // ok
/**
* Expected errors
*/
const result5 = deepPickFinal(foo, 'surname')
const result6 = deepPickFinal(foo, 'description')
const result7 = deepPickFinal(foo)