简体   繁体   English

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

[英]Why doesn't Object.keys return a keyof type in TypeScript?

Title says it all - why doesn't Object.keys(x) in TypeScript return the type Array<keyof typeof x> ?标题说明了一切——为什么 TypeScript 中的Object.keys(x)不返回Array<keyof typeof x>类型? That's what Object.keys does, so it seems like an obvious oversight on the part of the TypeScript definition file authors to not make the return type simply be keyof T .这就是Object.keys所做的,所以这似乎是 TypeScript 定义文件作者的一个明显疏忽,没有让返回类型简单地成为keyof T

Should I log a bug on their GitHub repo, or just go ahead and send a PR to fix it for them?我应该在他们的 GitHub 存储库上记录一个错误,还是继续发送 PR 来为他们修复它?

The current return type ( string[] ) is intentional.当前的返回类型 ( string[] ) 是有意的。 Why?为什么?

Consider some type like this:考虑这样的一些类型:

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

You write some code like this:你写一些这样的代码:

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

Let's ask a question:让我们问一个问题:

In a well-typed program, can a legal call to fn hit the error case?在一个类型良好的程序中,对fn的合法调用能否遇到错误情况?

The desired answer is, of course, "No".期望的答案当然是“否”。 But what does this have to do with Object.keys ?但这与Object.keys有什么关系?

Now consider this other code:现在考虑这个其他代码:

interface NamedPoint extends Point {
    name: string;
}

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

Note that according to TypeScript's type system, all NamedPoint s are valid Point s.请注意,根据 TypeScript 的类型系统,所有NamedPoint都是有效的Point

Now let's write a little more code :现在让我们再写一点代码

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

Our well-typed program just threw an exception!我们的良好类型程序只是抛出了一个异常!

Something went wrong here!这里出了点问题! By returning keyof T from Object.keys , we've violated the assumption that keyof T forms an exhaustive list, because having a reference to an object doesn't mean that the type of the reference isn't a supertype of the type of the value .通过从Object.keys返回keyof T ,我们违反了keyof T形成详尽列表的假设,因为对对象的引用并不意味着引用的类型不是对象类型的超类型值

Basically, (at least) one of the following four things can't be true:基本上,(至少)以下四件事之一不可能是真的:

  1. keyof T is an exhaustive list of the keys of T keyof TT的键的详尽列表
  2. A type with additional properties is always a subtype of its base type具有附加属性的类型始终是其基本类型的子类型
  3. It is legal to alias a subtype value by a supertype reference通过超类型引用为子类型值起别名是合法的
  4. Object.keys returns keyof T Object.keys返回keyof T

Throwing away point 1 makes keyof nearly useless, because it implies that keyof Point might be some value that isn't "x" or "y" .丢弃第 1 点会使keyof几乎无用,因为这意味着keyof Point可能是一些不是"x""y"的值。

Throwing away point 2 completely destroys TypeScript's type system.抛弃第 2 点完全破坏了 TypeScript 的类型系统。 Not an option.不是一个选择。

Throwing away point 3 also completely destroys TypeScript's type system.抛弃第 3 点也完全破坏了 TypeScript 的类型系统。

Throwing away point 4 is fine and makes you, the programmer, think about whether or not the object you're dealing with is possibly an alias for a subtype of the thing you think you have.扔掉第 4 点很好,让你,程序员,考虑你正在处理的对象是否可能是你认为你拥有的东西的子类型的别名。

The "missing feature" to make this legal but not contradictory is Exact Types , which would allow you to declare a new kind of type that wasn't subject to point #2.使此合法但不矛盾的“缺失功能”是Exact Types ,它允许您声明一种不受第 2 点约束的新类型 If this feature existed, it would presumably be possible to make Object.keys return keyof T only for T s which were declared as exact .如果存在此功能,则大概可以使Object.keys仅针对声明为的T s 返回keyof T精确


Addendum: Surely generics, though?附录:当然是泛型,但是?

Commentors have implied that Object.keys could safely return keyof T if the argument was a generic value.评论者暗示Object.keys可以安全地返回keyof T如果参数是通用值。 This is still wrong.这仍然是错误的。 Consider:考虑:

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

or this example, which doesn't even need any explicit type arguments:或者这个例子,它甚至不需要任何显式类型参数:

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

For a workaround in cases when you're confident that there aren't extra properties in the object you're working with, you can do this:如果您确信您正在使用的对象中没有额外的属性,您可以这样做:

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

You can extract this to a function if you like:如果您愿意,可以将其提取到函数中:

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

As a bonus, here's Object.entries , pulled from a GitHub issue with context on why this isn't the default :作为奖励,这里的Object.entries是从GitHub 问题中提取的,其中包含有关为什么这不是默认值的上下文

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

This is the top hit on google for this type of issue, so I wanted to share some help on moving forwards.这是这类问题在谷歌上的热门话题,所以我想分享一些关于前进的帮助。

These methods were largely pulled from the long discussions on various issue pages which you can find links to in other answers/comment sections.这些方法主要是从各种问题页面上的长时间讨论中提取的,您可以在其他答案/评论部分找到链接。

So, say you had some code like this :所以,假设你有一些这样的代码

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

Here are a few ways to move forwards:以下是一些前进的方法:

  1. If you don't need the keys and really just need the values, use .entries() or .values() instead of iterating over the keys.如果您不需要键而只需要值,请使用.entries().values()而不是遍历键。

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

     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. Re-cast your type (this one helps a lot for not having to rewrite much code)重铸你的类型(这对于不必重写太多代码很有帮助)

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

Which one of these is the most painless is largely up to your own project.其中哪一个是最轻松的,很大程度上取决于您自己的项目。

I'll try to explain why object keys cannot return keyof T while being safe with some simple example我将尝试用一些简单的例子来解释为什么对象键在安全的情况下不能返回 keyof 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
}

Possible solution可能的解决方案

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

In loops to get around this:在循环中解决这个问题:

  let key2: YourType
  for (key2 in obj) {
    obj[key2]
  }

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM