使用 Object.entries 时保留类型

UK_*_*fka 9 javascript typescript reactjs

我对 TypeScript 相当陌生,所以我正在升级我的旧项目以使用它。

但是,我不确定在某些数据上调用 Object.entries 时如何保留正确的类型。

代码沙盒示例

举个例子:

级别.tsx:

  const UnpassableTileComponents = useMemo(() => 
    Object.entries(levelData[`level_${gameLevel}`].tiles.unpassable_tiles).map(([tileType, tiles]) => (
      tiles.map(([leftPos, topPos], index) => (
        <UnpassableTile
          key={`${tileType}_${index}`}
          leftPos={leftPos * 40}
          topPos={topPos * 40}
          tileType={tileType}
        />
      ))
    )
  ).flat(), [gameLevel])
Run Code Online (Sandbox Code Playgroud)

levelData.tsx:

import levelJSON from "./levelJSON.json";

interface ILevelJSON {
  [key: string]: Level;
}

interface Level {
  tiles: Tiles;
}

interface Tiles {
  unpassable_tiles: UnpassableTiles;
}

interface UnpassableTiles {
  rock: Array<number[]>;
  tree: Array<number[]>;
}

export default levelJSON as ILevelJSON;
Run Code Online (Sandbox Code Playgroud)

levelJSON.json:

{
  "level_1": {
    "tiles": {
      "unpassable_tiles": {
        "rock": [[0, 0]],
        "tree": [[2, 0]]
      }
    }
  },
  "level_2": {
    "tiles": {
      "unpassable_tiles": {
        "rock": [[]],
        "tree": [[]]
      }
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

在上面的例子中,tiles代表一个数组的数组,每个数组都有两个数字。因此,[leftPos, topPos] 都应该输入为数字。然而,在 Level.tsx 中,它们具有 any 属性。我可以通过以下方式得到我想要的结果:

  const UnpassableTileComponents = useMemo(() => 
    Object.entries(levelData[`level_${gameLevel}`].tiles.unpassable_tiles).map(([tileType, tiles]) => (
      tiles.map(([leftPos, topPos] : number[], index: number) => (
        <UnpassableTile
          key={`${tileType}_${index}`}
          leftPos={leftPos * 40}
          topPos={topPos * 40}
          tileType={tileType}
        />
      ))
Run Code Online (Sandbox Code Playgroud)

但是 number[] 不应该被推断出来吗?

任何意见,将不胜感激。

jca*_*alz 12

这与问题相关,例如为什么 Object.keys 在 TypeScript 中不返回 keyof 类型?。这两个问题的答案是 TypeScript 中的对象类型并不精确(请参阅microsoft/TypeScript#12936);对象类型的值允许具有编译器不知道的额外属性。这允许接口和类继承,这是非常有用的。但这可能会导致混乱。

例如,如果我有一个nameHavertype值{name: string},我知道它有一个name属性,但我不知道它只有一个name属性。所以我不能说那Object.entries(nameHaver)会是Array<["name", string]>

interface NameHaver { name: string }
declare const nameHaver: NameHaver;
const entries: Array<["name", string]> = Object.entries(nameHaver); // error here: why?
entries.map(([k, v]) => v.toUpperCase()); 
Run Code Online (Sandbox Code Playgroud)

如果nameHaver不仅仅有一个name属性怎么办,例如:

interface NameHaver { name: string }
class Person implements NameHaver { constructor(public name: string, public age: number) { } }
const nameHaver: NameHaver = new Person("Alice", 35);
const entries: Array<["name", string]> = Object.entries(nameHaver); // error here: ohhh
entries.map(([k, v]) => v.toUpperCase());  // explodes at runtime!
Run Code Online (Sandbox Code Playgroud)

哎呀。我们假设的nameHaver值总是一个string,但其中一个是一个number,这会不满意toUpperCase()。唯一安全的假设Object.entries()是生成Array<[string, unknown]>(尽管标准库使用了Array<[string, any]>替代)。


所以,我们能做些什么?好吧,如果您碰巧知道并且绝对确定某个值仅具有编译器已知的键,那么您可以编写自己的类型Object.entries()并使用它......并且您需要非常小心。这是一种可能的输入:

type Entries<T> = { [K in keyof T]: [K, T[K]] }[keyof T];
function ObjectEntries<T extends object>(t: T): Entries<T>[] {
  return Object.entries(t) as any;
}
Run Code Online (Sandbox Code Playgroud)

as any是一种类型断言,它抑制了对 的正常抱怨Object.entries()。该类型Entries<T>是一个映射类型,我们立即查找它以生成已知条目的并集:

const entries = ObjectEntries(nameHaver);
// const entries: ["name", string][]
Run Code Online (Sandbox Code Playgroud)

这与我之前为 手动编写的类型相同entries。如果您在代码中使用ObjectEntries而不是,它应该“解决”您的问题。Object.entries但请记住,您所依赖的事实是,您正在迭代其条目的对象没有未知的额外属性。如果有人添加了非类型的额外属性number[]unpassable_tiles则可能会在运行时遇到问题。


Playground 代码链接


jer*_*red 2

@jcalz 的出色回答解释了为什么你想做的事情如此棘手。如果您想保持底层架构和 JSON 相同,他的方法可能会起作用。但我要指出的是,您可以通过以不同的方式构建数据来回避整个问题。我认为这将使您的开发人员体验更好,并澄清您的数据是什么

您遇到的基本问题之一是,在您的情况下,您试图将成对的地图key: value视为某种不可通行的图块列表。Object.entries但仅仅为了获取无法通过的图块类型而使用它本质上是笨拙且令人困惑的。

为什么不定义ImpassableTile为一种类型,并将不可通行的瓷砖列表定义为该类型的数组?从概念上讲,这更好地匹配数据实际表示的内容。它还Object.entries完全回避了它的困难,并使数据的迭代变得更加简单和清晰。

// levelData.ts
import levelJSON from "./levelJSON.json";

interface ILevelJSON {
  [key: string]: Level;
}

interface Level {
  tiles: Tiles;
}

export type UnpassableType = "rock" | "tree";

type UnpassableTile = {
  type: UnpassableType;
  position: number[];
};

interface Tiles {
  unpassable_tiles: UnpassableTile[];
}

export default levelJSON as ILevelJSON;
Run Code Online (Sandbox Code Playgroud)

为了正确匹配新接口,您还需要修改 levelJSON.json。但请注意,它更干净,您不需要在 level_2 中为岩石或树木定义空数组,这些根本不存在:

{
  "level_1": {
    "tiles": {
      "unpassable_tiles": [
        { "type": "rock", "position": [0, 0] },
        { "type": "rock", "position": [2, 0] },
        { "type": "tree", "position": [2, 2] }
      ]
    }
  },
  "level_2": {
    "tiles": {
      "unpassable_tiles": []
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

现在,您可以非常轻松地映射不可通行的图块、其类型以及关联的位置数据,同时保留完整的类型推断和安全性。在我看来,它看起来更加清晰易懂。

// App.tsx
const UnpassableTileComponents = React.useMemo(() => {
  return levelData[`level_1`].tiles.unpassable_tiles.map(
    ({ type, position: [leftPos, topPos] }) => (
      <UnpassableTile
        key={`level_1_${type}_${leftPos}_${topPos}`}
        leftPos={leftPos}
        topPos={topPos}
        tileType={type}
      />
    )
  );
}, []);
Run Code Online (Sandbox Code Playgroud)

https://codesandbox.io/s/goofy-snyder-u9x60?file=/src/App.tsx


您可以进一步将这一理念扩展到如何构建关卡及其界面。为什么不是levelJSON一个对象数组Level,每个对象都有一个名称和一组图块?

interface Tiles {
  unpassable_tiles: UnpassableTile[];
}

interface Level {
  name: string;
  tiles: Tiles;
}

export type UnpassableType = "rock" | "tree";

type UnpassableTile = {
  type: UnpassableType;
  position: number[];
};
Run Code Online (Sandbox Code Playgroud)

您相应的数据看起来会干净得多:

[
  {
    "name": "level_1",
    "tiles": {
      "unpassable_tiles": [
        { "type": "rock", "position": [0, 0] },
        { "type": "rock", "position": [2, 0] },
        { "type": "tree", "position": [2, 2] }
      ]
    }
  },
  {
    "name": "level_2",
    "tiles": {
      "unpassable_tiles": []
    }
  }
]
Run Code Online (Sandbox Code Playgroud)

迭代它会变得更加清晰:

const level = levelData[0];

const UnpassableTileComponents = React.useMemo(() => {
  return level.tiles.unpassable_tiles.map(
    ({ type, position: [leftPos, topPos] }) => (
      <UnpassableTile
        key={`${level.name}_${type}_${leftPos}_${topPos}`}
        leftPos={leftPos}
        topPos={topPos}
        tileType={type}
      />
    )
  );
}, [level]);
Run Code Online (Sandbox Code Playgroud)

https://codesandbox.io/s/hopeful-grass-dnohi?file=/src/App.tsx