【问题标题】:Typed Indexing variable is not of the same type as the object being indexed类型化索引变量与被索引的对象的类型不同
【发布时间】:2022-04-17 03:31:41
【问题描述】:

我有一个稍微复杂的对象,它在对象的最顶层以及对象的最深层都有键入的键,我似乎无法索引第二个键入的键对象。

这是我的对象和我的接口(简化):

const obj = {
  keyA: {
    variants: {
      "0": false,
      "1": false
    }
  },
  keyB: {
    variants: {
      "0-0": false,
      "0-1": false
    }
  }
}

type Obj = typeof obj;
type ObjKeys = keyof Obj;
type ObjVariants<T extends ObjKeys> = Obj[T]["variants"];

这是无法使用 defaultVariant 正确索引 selectedKeys.variant 的函数。

function getVariant<T extends ObjKeys>(key: T, defaultVariant: keyof ObjVariants<T>) {
  const selectedKey = obj[key];
  const selectedVariant = selectedKey.variants[defaultVariant];
}

我得到的错误是:

Type 'keyof { keyA: { variants: { 0: boolean; 1: boolean; }; }; keyB: { variants: { "0-0": boolean; "0-1": boolean; }; }; }[T]["variants"]' cannot be used to index type '{ 0: boolean; 1: boolean; } | { "0-0": boolean; "0-1": boolean; }'.(2536)

好像和这两个问题有关:https://github.com/microsoft/TypeScript/issues/21760https://github.com/microsoft/TypeScript/issues/36631 但我不确定第二个的“解决方法”是否适合我,我觉得我错过了一些东西

这里是游乐场的链接:https://www.typescriptlang.org/play/index.html?ssl=1&ssc=1&pln=65&pc=3#code/MYewdgzgLgBCBGArGBeGBvAUDGBrApgJ4CCAXBtjjAG4CGATgJa1hQTlZVUBEADN+QBmtADYR8AGkpduARgExhY-NJgBfSmqk4ChAEIdVdJizaGuOPgFp+Q0eO0Xr8u8tUacGjZiiEADvgwAPJIqDC+ASCCcEgA3D7+gSGIANJEEGG6UcFxCQE5iABqDMysEAA8ACow+AAeUPhgACYZyWmEEAB8YckA2pUAur3cxqVs3APxmJiCAK5gwFCM4DAA5vhQxSasVTX1jS0F7V0AFLrklRIwTfjCsyKbJabkWdHJW2MVlZ0AlBQ4oEgsHEInwi3wTXaYQQiF6ukmlEB0BgILBDSaH1MYVR4MhRAAdKNTBBejc7g9MawERogA

有没有其他人遇到过类似的情况/有建议的解决方法?

PD:我尝试将严格类型添加到 obj 并发现与使用 typeof obj 相同的结果。

【问题讨论】:

  • "T extends ObjKeys" 并不意味着 T 可以是 "keyA" XOR "keyB": T 也可以是 "keyA"|"keyB" 因为 "keyA "|"keyB" 实际上扩展了 ObjKeys。

标签: javascript typescript typescript-typings


【解决方案1】:

看起来像是设计行为,或者换句话说是设计限制。这样的函数很难保证类型安全,因为我们也可以传递子类型。例如T extends ObjKeys 也涵盖了联合keyA | keyB,这意味着这个约束不能假设它是变体之一。

为了完全设置约束,我们可以采用以下方法:

function getVariant
<O extends { [K in K1]: { variants: { [KK in K2]: Obj[K1]["variants"][KK] } } }
, K1 extends keyof Obj
, K2 extends keyof Obj[K1]["variants"]>
  (o: O, key: K1, defaultVariant: K2) {
  const selectedKey = o[key].variants;
  const selectedVariant = selectedKey[defaultVariant];
}
// using
getVariant(obj, 'keyA', '0')

如您所见,我制作了附加参数以缩小键 K1K2 的类型。最重要的一行是{ [K in K1]: { variants: { [KK in K2]: Obj[K1]["variants"][KK] } } }。这意味着我们处理涵盖两个键的类型,但定义仍然与我们的 obj 类型兼容。

我们仍然可以使用obj 作为外部常量,但是我们需要使用类型断言:

function getVariant<K1 extends keyof Obj, K2 extends keyof Obj[K1]["variants"]>
  (key: K1, defaultVariant: K2) {
  let _obj = obj as { [K in K1]: { variants: { [KK in K2]: Obj[K1]["variants"][KK] } } };
  const selectedKey = _obj[key].variants;
  const selectedVariant = selectedKey[defaultVariant];
}
// using
getVariant('keyA', '0')

为什么会这样?它之所以有效,是因为我们使用我们拥有/将拥有的确切类型静态设置类型。如果我们说 T extends A 并不意味着 T 是 A,也不意味着 T 是 A 的某个变体。通过说 T extends A 我们缩小范围,将类型 T 限制为可分配给 A ,因此我们并不严格。通过创建类型{[K in T]: X}, T extends Y,我们严格定义该类型将具有T 的精确键,并且它将严格地是从可能的可分配类型到Y 中的一种类型。如此严格的定义,相当于设置Obj['keyA']这样的静态值。

【讨论】:

  • 这完美!非常感谢。我还有一个小问题,这是getVariant在我没有添加第一个参数时的类型:getVariant(key: "keyA" | "keyB", defaultVariant: never): void。成功添加密钥变体自动完成后(太棒了!),我想知道您能否简要解释一下 defaultVariant 最初是如何变成 never 的。感谢您的回复!
  • 你能给我寄一些游乐场吗?很难回答您的问题。
【解决方案2】:

我参加聚会有点晚了,但以下概括可以解决问题:

type KeyOf<T> = keyof T;

function getVariant<
  O extends { [K in KeyOf<O>]: { variants: O[K]["variants"] } },
  K extends KeyOf<O>,
  V extends KeyOf<O[K]["variants"]>,
>(obj: O, key: K, variant: V): O[K]["variants"][V] {
  return obj[key].variants[variant];
}

Playground

【讨论】:

    猜你喜欢
    • 2011-06-20
    • 2018-04-01
    • 2017-02-23
    • 1970-01-01
    • 1970-01-01
    • 2017-06-11
    • 1970-01-01
    • 2011-03-03
    • 2020-08-29
    相关资源
    最近更新 更多