【问题标题】:Why doesn't Object.keys return a keyof type in TypeScript?为什么 Object.keys 在 TypeScript 中不返回 keyof 类型?
【发布时间】:2019-07-27 11:21:35
【问题描述】:

标题说明了一切 - 为什么 TypeScript 中的 Object.keys(x) 不返回类型 Array<keyof typeof x>?这就是 Object.keys 所做的事情,因此 TypeScript 定义文件的作者似乎有明显的疏忽,没有将返回类型简单地设为 keyof T

我应该在他们的 GitHub 存储库上记录错误,还是直接发送 PR 为他们修复?

【问题讨论】:

  • 我今天打开和关闭了一个与此主题相关的 PR。我的 PR 只关注键来自字符串枚举的情况。在这种精确的情况下,继承似乎不可行。在重新打开它之前我需要仔细检查github.com/Microsoft/TypeScript/pull/30228
  • FTR: ^ PR 从未合并

标签: typescript


【解决方案1】:

当前返回类型 (string[]) 是有意的。为什么?

考虑这样的一些类型:

interface Point {
    x: number;
    y: number;
}

你写一些这样的代码:

function fn(k: keyof Point) {
    if (k === "x") {
        console.log("X axis");
    } else if (k === "y") {
        console.log("Y axis");
    } else {
        throw new Error("This is impossible");
    }
}

让我们问一个问题:

在一个类型良好的程序中,对fn 的合法调用能否遇到错误情况?

想要的答案当然是“不”。但是这和Object.keys有什么关系呢?

现在考虑这个其他代码:

interface NamedPoint extends Point {
    name: string;
}

const origin: NamedPoint = { name: "origin", x: 0, y: 0 };

请注意,根据 TypeScript 的类型系统,所有NamedPoints 都是有效的Points。

现在让我们再写一点代码

function doSomething(pt: Point) {
    for (const k of Object.keys(pt)) {
        // A valid call iff Object.keys(pt) returns (keyof Point)[]
        fn(k);
    }
}
// Throws an exception
doSomething(origin);

我们的良好类型程序刚刚抛出异常!

这里出了点问题! 通过从Object.keys 返回keyof T,我们违反了keyof T 构成一个详尽列表的假设,因为对对象的引用并不意味着引用的类型 '不是值的类型的超类型。

基本上,(至少)以下四件事之一不可能是真的:

  1. keyof TT 的键的详尽列表
  2. 具有附加属性的类型始终是其基本类型的子类型
  3. 通过超类型引用为子类型值起别名是合法的
  4. Object.keys 返回keyof T

丢弃第 1 点会使keyof 几乎毫无用处,因为这意味着keyof Point 可能不是"x""y" 的某个值。

丢弃第 2 点完全破坏了 TypeScript 的类型系统。不是一个选项。

丢弃第 3 点也完全破坏了 TypeScript 的类型系统。

扔掉第 4 点很好,让你,程序员,想想你正在处理的对象是否可能是你认为你拥有的东西的子类型的别名。

使这个合法但不矛盾的“缺失功能”是Exact Types,这将允许您声明一个不受约束的新类型种类到第 2 点。如果存在此功能,则大概可以使Object.keys 仅针对声明为精确Ts 返回keyof T


附录:当然是泛型?

评论者暗示 Object.keys 可以安全地返回 keyof T 如果参数是一个通用值。这仍然是错误的。考虑:

class Holder<T> {
    value: T;
    constructor(arg: T) {
        this.value = arg;
    }

    getKeys(): (keyof T)[] {
        // Proposed: This should be OK
        return Object.keys(this.value);
    }
}
const MyPoint = { name: "origin", x: 0, y: 0 };
const h = new Holder<{ x: number, y: number }>(MyPoint);
// Value 'name' inhabits variable of type 'x' | 'y'
const v: "x" | "y" = (h.getKeys())[0];

或者这个例子,它甚至不需要任何显式类型参数:

function getKey<T>(x: T, y: T): keyof T {
    // Proposed: This should be OK
    return Object.keys(x)[0];
}
const obj1 = { name: "", x: 0, y: 0 };
const obj2 = { x: 0, y: 0 };
// Value "name" inhabits variable with type "x" | "y"
const s: "x" | "y" = getKey(obj1, obj2);

【讨论】:

  • 但是,在排除第 3 点的情况下,例如T 被推断并保证准确:const f: &lt;T&gt;(t: T) =&gt; void = (t) =&gt; { Object.keys(t).forEach(k =&gt; t[k]) }。我的代码中有很多这样的地方,我真的希望 Object.keys() 返回 (keyof T)[]。
  • 正如 arthem 还指出的那样,混淆来自这样一个事实,即 10 次中有 9 次最终会以某种方式使用 keyof T 的类型断言来对 @ 的结果做任何有用的事情987654353@。你可能会争辩说最好明确一点,这样你就会更清楚你所承担的风险,但可能 9/10 开发人员只会添加类型断言,而不知道你强调的问题..
  • 为什么Object.keys&lt;T&gt;(c extends T obj) 不能简单地过滤 obj (type c) 返回 T 的键?
  • 如果有人真的被类型转换 Object.keys(foo) 到 Array 搞砸了,其中来自 Object.keys 的运行时值实际上包含的键比编译时知道的要多-时间,很多人都喜欢将此代码视为示例。请分享一下
  • 通常,这仅用于能够遍历对象,但这使得它不可能,因为Object.keys(product).forEach((key) =&gt; { // do something with product[key] but leads to error 'has type any because string cannot be used to query ...' }); 会导致错误。
【解决方案2】:

如果您确信您正在使用的对象中没有额外的属性,您可以这样做:

const obj = {a: 1, b: 2}
const objKeys = Object.keys(obj) as Array<keyof typeof obj>
// objKeys has type ("a" | "b")[]

如果你愿意,你可以把它提取到一个函数中:

const getKeys = <T>(obj: T) => Object.keys(obj) as Array<keyof T>

const obj = {a: 1, b: 2}
const objKeys = getKeys(obj)
// objKeys has type ("a" | "b")[]

作为奖励,这里是 Object.entries,来自 a GitHub issue with context on why this isn't the default

type Entries<T> = {
  [K in keyof T]: [K, T[K]]
}[keyof T][]

function entries<T>(obj: T): Entries<T> {
  return Object.entries(obj) as any;
}

【讨论】:

    【解决方案3】:

    这是此类问题在谷歌上的热门话题,所以我想分享一些关于前进的帮助。

    这些方法主要来自各个问题页面上的长时间讨论,您可以在其他答案/评论部分找到链接。

    所以,假设你有一些code like this

    const obj = {};
    Object.keys(obj).forEach((key) => {
      obj[key]; // blatantly safe code that errors
    });
    

    以下是一些前进的方法:

    1. 如果唯一的问题是访问器,请使用 .entries().values() 而不是遍历键。

      const obj = {};
      Object.values(obj).forEach(value => value);
      Object.entries(obj).forEach([key, value] => value);
      
    2. 创建一个辅助函数:

      function keysOf<T extends Object>(obj: T): Array<keyof T> {
        return Array.from(Object.keys(obj)) as any;
      }
      
      const obj = { a: 1; b: 2 };
      keysOf(obj).forEach((key) => obj[key]); // type of key is "a" | "b"
      
    3. 重铸你的类型(这对不必重写太多代码很有帮助)

      const obj = {};
      Object.keys(obj).forEach((_key) => {
        const key = _key as keyof typeof obj;
        obj[key];
      });
      

    其中哪一项最轻松,很大程度上取决于您自己的项目。

    【讨论】:

    • 我最近猛烈抨击了这一点,并想再增加一个选项:转换为Map。转换为 Map 以支持旧代码是一个巨大的麻烦,但如果你正在编写新的东西,它很容易使用它,就像 Object.keys 模式我 - 可能你正在阅读这个 - 是习惯使用。 const myMap: Map&lt;SomeType, SomeOtherType&gt; = new Map(),然后用 myMap.forEach((val, key) =&gt; {... and TypeScript is happy here ...}) 循环遍历它
    【解决方案4】:

    可能的解决方案

    const isName = <W extends string, T extends Record<W, any>>(obj: T) =>
      (name: string): name is keyof T & W =>
        obj.hasOwnProperty(name);
    
    const keys = Object.keys(x).filter(isName(x));
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-01-20
      • 1970-01-01
      • 2019-08-21
      • 2021-07-19
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多