我想声明一个函数,该函数可以接受一个对象以及一组嵌套的属性键,并将嵌套值的类型派生为该函数的返回类型。
例如
const value = byPath({ state: State, path: ['one', 'two', 'three'] });
// return type == State['one']['two']['three']
const value2 = byPath({ state: State, path: ['one', 'two'] });
// return type == State['one']['two']
我能做到的最好的事情如下,但是它比我希望的更加冗长,并且我必须为每个嵌套级别添加一个函数重载。
export function byPath<
K1 extends string,
R
>({ state, path }: {
state: {[P1 in K1]?: R},
path: [K1]
}): R;
export function byPath<
K1 extends string,
K2 extends string,
R
>({ state, path }: {
state: {[P1 in K1]?: {[P2 in K2]?: R}},
path: [K1, K2]
}): R;
export function byPath<
K1 extends string,
K2 extends string,
K3 extends string,
R
>({ state, path }: {
state: {[P1 in K1]?: {[P2 in K2]?: {[P3 in K3]?: R}}},
path: [K1, K2, K3]
}): R;
export function byPath<R>({ state, path }: { state: State, path: string[] }): R | undefined {
// do the actual nested property retrieval
}
是否有更简单/更好的方法来做到这一点?
答案 0 :(得分:3)
不幸的是,TypeScript当前不允许任意的recursive type functions,这是您想要遍历键列表,向下钻取到对象类型并给出与之对应的嵌套属性类型的内容密钥列表。您可以做一些事情,但是很混乱。
因此,您将必须选择一些最大级别的嵌套并为此编写代码。这是您的函数可能不使用重载的类型签名:
type IfKey<T, K> = [K] extends [keyof T] ? T[K] : T;
declare function byPath<T0,
K1 extends keyof T0 | undefined, T1 extends IfKey<T0, K1>,
K2 extends keyof T1 | undefined, T2 extends IfKey<T1, K2>,
K3 extends keyof T2 | undefined, T3 extends IfKey<T2, K3>,
K4 extends keyof T3 | undefined, T4 extends IfKey<T3, K4>,
K5 extends keyof T4 | undefined, T5 extends IfKey<T4, K5>,
K6 extends keyof T5 | undefined, T6 extends IfKey<T5, K6>
>({ state, path }: { state: T0, path: [K1?, K2?, K3?, K4?, K5?, K6?] }): T6;
请注意,您可以根据需要轻松地将其扩展到六层以上的嵌套。
工作方式:有两种类型参数...键类型(命名为K1
,K2
等)和对象类型(命名为T0
,{{ 1}}等)。 T1
属性的类型为state
,路径为键类型的tuple with optional elements。每个键类型要么是先前对象类型的键,要么是T0
。如果键未定义,则下一个对象类型与当前对象类型相同;否则,返回下一个对象类型。否则就是相关属性的类型。因此,只要键类型变为并保持为undefined
,对象类型就变为并保持为最后一个相关属性类型...,而最后一个对象类型(上述undefined
)是该函数的返回类型。
让我们举个例子:如果T6
是T0
,则{a: {b: string}, c: {d: string}}
必须是K1
,'a'
或'd'
中的一个。假设undefined
是K1
。那么'a'
是T1
。现在{b: string}
必须是K2
或'b'
。假设undefined
是K2
。那么'b'
是T2
。现在string
必须在K3
或keyof string
中。 (因此undefined
可以是K3
或任何"charAt"
方法和属性)。假设string
是K3
。那么undefined
是T3
(因为它与string
相同)。并且,如果T2
,K4
和K5
的其余全部为K6
,则undefined
,T4
和T5
只是T6
。函数返回string
。
因此,如果您拨打此电话:
T6
然后,const ret = byPath({state: {a: {b: "hey"}, c: {d: "you"} }, path: ['a', 'b'] });
将被推断为T0
,{a: {b: string}, c: {d: string}
将是K1
,'a'
将是K2
,而{{1} }至'b'
将全部为K3
。这是上面的示例,因此K6
将是undefined
。因此T6
的类型为string
。
如果您输入了错误的密钥,上述功能签名也会对您大喊:
ret
该错误是有道理的,因为string
无效。以下内容还会对您大喊:
const whoops = byPath({ state: { a: { b: "hey" }, c: { d: "you" } }, path: ['a', 'B'] });
// error! type "B" is not assignable to "b" | undefined: ----------------------> ~~~
第一个错误正是您所期望的;第二个有点奇怪,因为B
很好。但是编译器现在不知道对const alsoWhoops = byPath({ state: { a: { b: "hey" }, c: { d: "you" } }, path: ['A', 'b'] });
// error! type "A" is not assignable to "a" | "c" | undefined: ---------------> ~~~
// also error! Type "b" is not assignable to "a" | "c" | undefined ?! -------------> ~~~
会有什么期望,因此它的行为就像"b"
是keyof T['A']
一样。如果您解决了第一个错误,则第二个错误将消失。可能有一些方法可以更改K1
签名来避免这种情况,但是对我来说似乎很小。
无论如何,希望对您有所帮助或给您一些想法。祝你好运!
编辑:如果您关心该错误的第二条错误消息,则可以使用稍微复杂一点的内容:
undefined
几乎相同,除了在键不匹配它们应该匹配的地方出错的情况下。