簡體   English   中英

為什么 Object.keys 在 TypeScript 中不返回 keyof 類型?

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

標題說明了一切——為什么 TypeScript 中的Object.keys(x)不返回Array<keyof typeof x>類型? 這就是Object.keys所做的,所以這似乎是 TypeScript 定義文件作者的一個明顯疏忽,沒有讓返回類型簡單地成為keyof T

我應該在他們的 GitHub 存儲庫上記錄一個錯誤,還是繼續發送 PR 來為他們修復它?

當前的返回類型 ( 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 的類型系統,所有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);

我們的良好類型程序只是拋出了一個異常!

這里出了點問題! 通過從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僅針對聲明為的T s 返回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);

如果您確信您正在使用的對象中沒有額外的屬性,您可以這樣做:

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

這是這類問題在谷歌上的熱門話題,所以我想分享一些關於前進的幫助。

這些方法主要是從各種問題頁面上的長時間討論中提取的,您可以在其他答案/評論部分找到鏈接。

所以,假設你有一些這樣的代碼

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

其中哪一個是最輕松的,很大程度上取決於您自己的項目。

我將嘗試用一些簡單的例子來解釋為什么對象鍵在安全的情況下不能返回 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
}

可能的解決方案

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

在循環中解決這個問題:

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

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM