TypeScript:是否可以安全地访问给定键数组的对象的嵌套属性?这可以以类型安全且可组合的方式完成吗?

标签 typescript typescript-generics

我想编写一个函数,它从给定属性键数组的对象中获取值。它看起来像这样:

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[]或类型断言以使编译器接受它。


Playground link to code

关于TypeScript:是否可以安全地访问给定键数组的对象的嵌套属性?这可以以类型安全且可组合的方式完成吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/63650858/

相关文章:

typescript - 你能让 TS 从被赋值的值中推断出变量的泛型类型参数吗?

typescript - 如何在 Typescript 上对未知类型使用条件

javascript - ES6 mixin 的 Typescript 定义

node.js - 响应拦截器的 CatchError 函数执行多次 - Angular 6

node.js - 在 nodejs 项目 VSCode 中禁用 DOM 库类型定义

typescript - 根据世博会上的用户选择更改语言

typescript - 如何使用对枚举通用的类扩展通用接口(interface)?

javascript - TypeScript - 获取泛型类参数的类型

typescript :使用泛型函数获取嵌套对象的值

javascript - 为什么当我尝试初始化多个组件时,Firefox 和其他浏览器会卡住,但在 Chrome 上却不会?