我想编写一个函数,它从给定属性键数组的对象中获取值。它看起来像这样:
function getValue<O, K extends ObjKeys<O>>(obj: O, keys: K): ObjVal<O,K> {
let out = obj;
for (const k in keys) out = out[k]
return out
}
我希望这个函数像这样工作:
type Foo = {
a: number
b: string
c: {
lark: boolean
wibble: string
}
}
let o: Foo = {
a: 1,
b: "hi",
c: {
lark: true,
wibble: "there"
}
}
// I'd like these to type check (and return the expected values):
getValue(o, ['a']) // returns 1
getValue(o, ['b']) // returns "hi"
getValue(o, ['c','lark']) // returns true
// I'd like these _not_ to type check:
getValue(o, ['a','b'])
getValue(o, ['d'])
重要的是,我希望有一个可用的类型(如上例中的 ObjKeys<O>
),以便我可以从其他函数轻松使用此函数,同时保留类型。例如我可能想做这样的事情:
function areValuesEqual<O>(obj: O, oldObj: O, keys: ObjKeys<O>) {
let value = getValue(obj, keys)
let oldValue = getValue(oldObj, keys)
return value === oldValue ? true : false
}
这个函数需要一些键并将它们传递给我们的 getValue
上面,理想情况下它会进行所有类型检查,因为对象 O
和 key ObjKeys<O>
都是 getValue
的有效参数内部调用的函数。
这扩展到返回 getValue
给出的值;我可能也想做这样的事情:
function doSomethingAndThenGetValue<O>(obj: O, oldObj: O, keys: ObjKeys<O>): ObjVal<O> {
let value = getValue(obj, keys)
console.log("Value obtained is:", value)
return value
}
这也使用类似 ObjVal<O>
的内容知道返回类型是什么,因此需要进行全面的类型检查。
是否有解决方案,或者根本没有办法在 TypeScript 中执行此类操作(撰写本文时为版本 4)?
迄今为止我拥有的最好的:
我可以定义一个允许嵌套访问的函数,如下所示:
function getValue<
O extends object,
K1 extends keyof O
>(obj: O, keys: [K1]): O[K1]
function getValue<
O extends object,
K1 extends keyof O,
K2 extends keyof O[K1]
>(obj: O, keys: [K1,K2]): O[K1][K2]
function getValue<
O extends object,
K1 extends keyof O,
K2 extends keyof O[K1],
K3 extends keyof O[K1][K2]
>(obj: O, keys: [K1,K2,K3]): O[K1][K2][K3]
function getValue<O>(obj: O, keys: Key | (Key[])): unknown {
let out = obj;
for (const k in keys) out = out[k]
return out
}
type Key = string | number | symbol
然后,当我尝试访问值时,会正确进行类型检查(在本例中最多为 3 层)。
但是,当我想在保持类型安全的同时使用另一个函数时,我会遇到一点困难:
function areValuesEqual<O>(obj: O, oldObj: O, keys: ????) {
let value = getValue(obj, keys)
let oldValue = getValue(oldObj, keys)
return value === oldValue ? true : false
}
function doSomethingAndThenGetValue<O>(obj: O, oldObj: O, keys: ????): ???? {
let value = getValue(obj, keys)
console.log("Value obtained is:", value)
return value
}
我不确定可以用什么来代替 ????
告诉 TypeScript 类型如何相互关联,以便进行类型检查。我是否可以避免每次要编写上述函数时都必须重写大量重载列表,但仍能获得我想要的类型检查?
最佳答案
这已经接近我可以从类型系统中获得的极限了。 TypeScript 4.1 将支持recursive conditional types ,但即使有了它们,我想你很可能会遇到循环错误,“类型实例化太深”错误,或任何尝试使用 getValue()
的任何东西的奇怪错误。一般而言。所以我不确定我是否真的会建议您使用我下面要写的内容:
在 another question我写了如何说服编译器为您提供对象的所有有效键路径的并集,以元组表示。它看起来像这样:
type Cons<H, T> = T extends readonly any[] ? [H, ...T] : never;
type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
11, 12, 13, 14, 15, 16, 17, 18, 19, 20, ...0[]]
type Paths<T, D extends number = 10> = [D] extends [never] ? never : T extends object ?
{ [K in keyof T]-?: [K] | (Paths<T[K], Prev[D]> extends infer P ?
P extends [] ? never : Cons<K, P> : never
) }[keyof T]
: [];
您可以验证一下
type FooPaths = Paths<Foo>;
// type FooPaths = ["a"] | ["b"] | ["c"] | ["c", "lark"] | ["c", "wibble"]
以下DeepIdx<T, KS>
的定义将给出关键路径 KS
处的属性类型(其中 KS extends Paths<T>
应该为 true),但仅适用于 TS4.1+:
type DeepIdx<T, KS extends readonly any[]> = KS extends readonly [infer K, ...infer KK] ?
K extends keyof T ? DeepIdx<T[K], KK> : never : T
您可以验证一下
type FooCWibble = DeepIdx<Foo, ["c", "wibble"]>;
// type FooCWibble = string
有了这些,你的getValue()
可以这样输入而不重载:
function getValue<O, KK extends Paths<O> | []>(
obj: O, keys: KK
): DeepIdx<O, KK> {
let out: any = obj;
for (const k in keys) out = out[k as any]
return out;
}
您可以验证这些是否有效:
const num = getValue(o, ['a']) // number
const str = getValue(o, ['b']) // string
const boo = getValue(o, ['c', 'lark']) // boolean
getValue(o, ['a', 'b']) // error!
// -------------> ~~~
// b is not assignable to lark | wibble
getValue(o, ['d']) // error!
// --------> ~~~
// d is not assignable to a | b | c
然后这个定义也适用于 areValuesEqual()
内部如果你给keys
类型Paths<O>
:
function areValuesEqual<O>(obj: O, oldObj: O, keys: Paths<O>) {
let value = getValue(obj, keys)
let oldValue = getValue(oldObj, keys)
return value === oldValue ? true : false
}
对于doSomethingAndThenGetValue()
你必须做 keys
通用,以便编译器知道会出现什么:
function doSomethingAndThenGetValue<O, K extends Paths<O>>(
obj: O,
oldObj: O,
keys: K
): DeepIdx<O, K> {
let value = getValue(obj, keys)
console.log("Value obtained is:", value)
return value
}
我可以准确地解释所有这些类型是如何工作的,但是它有点复杂,并且有一些专门为同轴类型推断而定制的构造,以便以正确的方式工作( | []
暗示元组上下文)或避免立即循环警告( Prev
元组用于在最大递归深度上放置调节器),并且我不知道详细解释一些我非常犹豫是否放入任何生产代码库的东西有多大用处。
出于您的目的,您可能只想在运行时更多地强制执行约束并执行类似 PropertyKey[]
的操作对于 keys
。里面执行areValuesEqual()
或者你可以只使用 any[]
或类型断言以使编译器接受它。
关于TypeScript:是否可以安全地访问给定键数组的对象的嵌套属性?这可以以类型安全且可组合的方式完成吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/63650858/