【问题标题】:Preserve Type when using Object.entries使用 Object.entries 时保留类型
【发布时间】:2020-09-15 02:54:37
【问题描述】:

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

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

CodeSandbox example

举个例子:

Level.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])

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;

levelJSON.json:

{
  "level_1": {
    "tiles": {
      "unpassable_tiles": {
        "rock": [[0, 0]],
        "tree": [[2, 0]]
      }
    }
  },
  "level_2": {
    "tiles": {
      "unpassable_tiles": {
        "rock": [[]],
        "tree": [[]]
      }
    }
  }
}

在上述情况下,tiles 表示一个数组 Array,每个数组有两个数字。 因此,[leftPos, topPos] 都应该输入为数字。但是,在 Level.tsx 中,它们具有任何属性。我可以通过以下方式获得我想要的结果:

  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}
        />
      ))

但无论如何不应该推断 number[] 吗?

任何建议将不胜感激。

【问题讨论】:

  • 欢迎来到 Stack Overflow!请考虑修改上述代码以构成minimal reproducible example 中提到的How to Ask。理想情况下,可以将某些东西放入像The TypeScript Playground 这样的独立 IDE 中,并允许想要帮助的人自己演示您的问题。祝你好运!
  • @jcalz 按要求添加:)

标签: javascript reactjs typescript


【解决方案1】:

这与Why doesn't Object.keys() return a keyof type in TypeScript? 之类的问题有关。两者的答案都是 TypeScript 中的对象类型不是 exact;对象类型的值允许有编译器不知道的额外属性。这允许接口和类继承,这非常有用。但这可能会导致混乱。

例如,如果我有一个 {name: string} 类型的值 nameHaver,我知道它有一个 name 属性,但我不知道它只有有一个 @987654331 @ 财产。所以我不能说Object.entries(nameHaver) 会是Array&lt;["name", string]&gt;

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

如果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!

哎呀。我们假设nameHaver 的值始终是string,但一个是number,它不会对toUpperCase() 感到满意。假设Object.entries() 产生的唯一安全的事情是Array&lt;[string, unknown]&gt;(尽管标准库使用Array&lt;[string, any]&gt; 代替)。


那么我们能做些什么呢?好吧,如果你碰巧知道并且绝对确定一个值只有编译器知道的键,那么你可以为 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;
}

as any 是一个type assertion,用于抑制对Object.entries() 的正常投诉。 Entries&lt;T&gt; 类型是 mapped type,我们立即 look up 生成已知条目的联合:

const entries = ObjectEntries(nameHaver);
// const entries: ["name", string][]

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


好的,希望对您有所帮助;祝你好运!

Playground link to code

【讨论】:

  • 我正在写类似的东西,但你的更全面。我还要提到你可以将[key: string]: Array&lt;number[]&gt;; 添加到UnpassableTiles 接口。他将失去对该接口键的严格执行,将其限制为“rock”或“tree”,但取决于他的用例,这可能无关紧要。
【解决方案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;

要正确匹配新界面,您还需要修改 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": []
    }
  }
}

现在您可以非常轻松地映射无法通过的图块、它们的类型和相关的位置数据,同时保留完整的类型推断和安全性。而且它看起来更加清晰易懂 IMO。

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

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

你对应的数据看起来会干净很多:

[
  {
    "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": []
    }
  }
]

并且对其进行迭代会变得更加清晰:

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

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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2023-01-10
    • 2020-05-25
    • 1970-01-01
    • 2021-11-20
    • 2020-06-15
    • 2020-11-18
    • 1970-01-01
    • 2022-07-21
    相关资源
    最近更新 更多