简体   繁体   English

使用 Object.entries 时保留类型

[英]Preserve Type when using Object.entries

I'm fairly new to TypeScript, so I'm in the process of upgrading my old projects to utilize it.我对 TypeScript 还很陌生,所以我正在升级我的旧项目以利用它。

However, I'm not sure how to preserve the correct Type when calling Object.entries on some data.但是,我不确定在对某些数据调用 Object.entries 时如何保留正确的类型。

CodeSandbox example代码沙盒示例

As an example:举个例子:

Level.tsx:级别.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:级别数据.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:级别JSON.json:

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

In the case of the above, tiles represents an Array of arrays, each with two numbers.在上述情况下,tile 表示一个 arrays 数组,每个数组有两个数字。 Therefore, [leftPos, topPos] should both be typed as number.因此,[leftPos, topPos] 都应该输入为数字。 However, in Level.tsx, they have properties of any.但是,在 Level.tsx 中,它们具有任何属性。 I could get my desired result with the following:我可以通过以下方式获得我想要的结果:

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

But shouldn't number[] be inferred anyways?但是无论如何不应该推断 number[] 吗?

Any advice would be appreciated.任何意见,将不胜感激。

This is related to questions like Why doesn't Object.keys() return a keyof type in TypeScript?这与为什么Object.keys()不返回 TypeScript 中的 keyof 类型之类的问题有关? . . The answer to both is that object types in TypeScript are not exact ;两者的答案是 TypeScript 中的 object 类型不准确 values of object types are allowed to extra properties not known about by the compiler. object 类型的值允许使用编译器不知道的额外属性。 This allows interface and class inheritance, which is very useful.这允许接口和class inheritance,这是非常有用的。 But it can lead to confusion.但这可能会导致混乱。

For example, if I have a value nameHaver of type {name: string} , I know it has a name property, but I don't know that it only has a name property.例如,如果我有一个{name: string}类型的值nameHaver ,我知道它有一个name属性,但我不知道它只有一个name属性。 So I can't say that Object.entries(nameHaver) will be Array<["name", string]> :所以我不能说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()); 

What if nameHaver has more than just a name property, as in:如果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!

Oops.哎呀。 We assumed that nameHaver 's values were always string , but one is a number , which will not be happy with toUpperCase() .我们假设nameHaver的值始终是string ,但其中一个是number ,这不会对toUpperCase()感到满意。 The only safe thing to assume that Object.entries() produces is Array<[string, unknown]> (although the standard library uses Array<[string, any]> instead).假设Object.entries()产生的唯一安全的事情是Array<[string, unknown]> (尽管标准库使用Array<[string, any]>代替)。


So what can we do?所以,我们能做些什么? Well, if you happen to know and are absolutely sure that a value has only the keys known about by the compiler, then you can write your own typing for Object.entries() and use that instead... and you need to be very careful with it.好吧,如果你碰巧知道并且绝对确定一个值只有编译器知道的键,那么你可以为Object.entries()编写自己的类型并使用它来代替......你需要非常小心它。 Here's one possible typing:这是一种可能的类型:

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

The as any is a type assertion that suppresses the normal complaint about Object.entries() . as any是一种类型断言,它抑制关于Object.entries()的正常抱怨。 The type Entries<T> is a mapped type that we immediately look up to produce a union of the known entries: Entries<T>类型是一个映射类型,我们立即查找它以生成已知条目的联合:

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

That is the same type I manually wrote before for entries .这与我之前为entries手动编写的类型相同。 If you use ObjectEntries instead of Object.entries in your code, it should "fix" your issue.如果您在代码中使用ObjectEntries而不是Object.entries ,它应该“解决”您的问题。 But do keep in mind you are relying on the fact that the object whose entries you are iterating has no unknown extra properties.但请记住,您所依赖的事实是,您正在迭代其条目的 object 没有未知的额外属性。 If it ever becomes the case that someone adds an extra property of a non- number[] type to unpassable_tiles , you might have a problem at runtime.如果有人向unpassable_tiles添加了一个非number[]类型的额外属性,那么您在运行时可能会遇到问题。


Okay, hope that helps;好的,希望有帮助; good luck!祝你好运!

Playground link to code Playground 代码链接

@jcalz's excellent answer explains why what you are trying to do is so tricky. @jcalz 的出色回答解释了为什么您尝试做的事情如此棘手。 His approach could work if you want to keep your underlying schemas and JSON the same.如果您想保持底层架构和 JSON 相同,他的方法可能会奏效。 But I will point out that you can sidestep the entire problem just by structuring your data differently.但我要指出,您可以通过不同的数据结构来回避整个问题。 I think that will make your developer experience, as well as the clarify of what your data is , better.我认为这将使您的开发人员体验更好,并且更清楚您的数据是什么

One of the fundamental problems you're having is that you're trying to treat a map of key: value pairs as, in your case, some sort of list of impassable tiles.您遇到的一个基本问题是,您试图将 map 的key: value对视为某种无法通过的瓷砖列表。 But it is inherently unwieldy and confusing to work with Object.entries just to get at your impassable tile types.但是使用Object.entries只是为了获得无法通过的瓷砖类型,它本质上是笨拙和令人困惑的。

Why not define ImpassableTile as a type, and the list of impassable tiles as an array of that type?为什么不将ImpassableTile定义为一种类型,而将不可通过的瓷砖列表定义为该类型的数组? That better matches, conceptually, what the data actually represents.从概念上讲,这更好地匹配数据实际代表的内容。 It also sidesteps Object.entries and its difficulties entirely, and makes iterating over the data more simple and clear.它还完全回避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;

To properly match the new interface, you'd need to modify levelJSON.json as well.要正确匹配新接口,您还需要修改 levelJSON.json。 But note that it's a lot cleaner and you'd don't need to define empty arrays for rocks or trees in level_2, those are simply absent:但请注意,它更干净,您不需要为 level_2 中的岩石或树木定义空的 arrays,这些根本不存在:

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

Now you can very easily map over your impassable tiles, their types, and associated position data, all while retaining full type inference and safety.现在,您可以非常轻松地 map 处理无法通过的图块、它们的类型以及相关的 position 数据,同时保留完整的类型推断和安全性。 And it looks a lot more clear and understandable IMO.它看起来更加清晰易懂,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 https://codesandbox.io/s/goofy-snyder-u9x60?file=/src/App.tsx


You can further extend this philosophy to how you structure your Levels and their interfaces.您可以进一步将此理念扩展到您如何构建关卡及其界面。 Why not have levelJSON be an array of Level objects, each with a name and set of tiles?为什么不让levelJSON成为Level对象的数组,每个对象都有一个名称和一组图块?

interface Tiles {
  unpassable_tiles: UnpassableTile[];
}

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

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

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

Your corresponding data would look a lot cleaner:您的相应数据看起来会更干净:

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

And iterating over it would become even more clear:迭代它会变得更加清晰:

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 https://codesandbox.io/s/hopeful-grass-dnohi?file=/src/App.tsx

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

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