为什么Object.keys不返回TypeScript中的keyof类型?

时间:2019-03-05 21:52:34

标签: typescript

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

我应该在他们的GitHub存储库上记录一个错误,还是继续发送PR为其进行修复?

4 个答案:

答案 0 :(得分:25)

当前返回类型(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有什么关系?

现在考虑以下 other 代码:

interface NamedPoint extends Point {
    name: string;
}

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

请注意,根据TypeScript的类型系统,所有NamedPoint是有效的Point

现在让我们再编写一些代码

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);

我们类型正确的程序只是引发了异常!

这里出问题了! 通过从keyof T返回Object.keys,我们违反了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,这将允许您声明一个新的种类类型的“ t受第2点的约束。如果存在此功能,则大概可以使Object.keys仅对声明为 exact keyof T个返回T

答案 1 :(得分:4)

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

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")[]

作为奖励,这是从 a GitHub issue with context on why this isn't the default 中提取的 Object.entries

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;
}

答案 2 :(得分:2)

我将通过一些简单的示例来说明为什么对象键在安全的同时不能返回T的键

// we declare base interface
interface Point {
  x: number;
  y: number;
}

// we declare some util function that expects point and iterates over keys
function getPointVelocity(point: Point): number {
  let velocity = 0;
  Object.keys(point).every(key => {
    // it seems Object.keys(point) will be ['x', 'y'], but it's not guaranteed to be true! (see below)
    // let's assume key is keyof Point
    velocity+= point[key];
  });

  return velocity;
}

// we create supertype of point
interface NamedPoint extends Point {
  name: string;
}

function someProcessing() {
  const namedPoint: NamedPoint = {
    x: 5,
    y: 3,
    name: 'mypoint'
  }

  // ts is not complaining as namedpoint is supertype of point
  // this util function is using object.keys which will return (['x', 'y', 'name']) under the hood
  const velocity = getPointVelocity(namedPoint);
  // !!! velocity will be string '8mypoint' (5+3+'mypoint') while TS thinks it's a number
}

答案 3 :(得分:2)

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

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

那么,假设您有一些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];
    });
    

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