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

ibps3vxo  于 2023-01-03  发布在  TypeScript
关注(0)|答案(4)|浏览(226)

标题说明了一切--为什么TypeScript中的Object.keys(x)不返回类型Array<keyof typeof x>?这正是Object.keys所做的,因此没有将返回类型简单地设置为keyof T似乎是TypeScript定义文件作者的一个明显疏忽。
我应该在他们的GitHub repo上记录一个bug,还是直接发送PR来为他们修复它?

ckx4rj1h

ckx4rj1h1#

当前返回类型(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 if Object.keys(pt) returns (keyof Point)[]
        fn(k);
    }
}
// Throws an exception
doSomething(origin);

我们的类型良好的程序刚刚抛出了一个异常!
这里出错了!从Object.keys返回keyof T,我们违反了keyof T形成一个穷举列表的假设,因为引用一个对象并不意味着引用的 * 类型 * 不是值的 * 类型 * 的超类型。
基本上,(至少)以下四件事中的一件不可能是真的:

  1. keyof TT密钥的详尽列表
    1.具有附加属性的类型始终是其基类型的子类型
    1.使用超类型引用作为子类型值的别名是法律的的
  2. Object.keys返回keyof T
    如果去掉点1,keyof就几乎没用了,因为这意味着keyof Point可能不是"x""y"
    丢弃第2点会完全破坏TypeScript的类型系统,这不是一个选项。
    丢弃第3点也会完全破坏TypeScript的类型系统。
    抛开第4点是很好的,这会让程序员考虑您正在处理的对象是否可能是您认为拥有的对象的子类型的别名。
    Exact Types是一个“缺少的特性”,它允许你声明一个新的类型,而不受第二点的约束。如果这个特性存在,那么大概可以让Object.keys只对声明为“精确”的T返回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);
d6kp6zgx

d6kp6zgx2#

如果您确信正在使用的对象中没有额外的属性,则可以执行以下操作:

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

mkshixfv3#

这是谷歌上关于这类问题的热门文章,所以我想分享一些关于前进的帮助。
这些方法很大程度上是从各种问题页面上的长时间讨论中提取出来的,你可以在其他答案/评论部分找到链接。
假设你有这样的代码:

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

1.创建辅助函数:

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"

1.重新转换类型(这一点很有帮助,因为不必重写太多代码)

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

其中哪一个是最无痛的主要取决于您自己的项目。

d4so4syb

d4so4syb4#

可能的解决办法

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

相关问题