typescript 使用对象项时保留类型

6ioyuze2  于 2023-01-14  发布在  TypeScript
关注(0)|答案(2)|浏览(126)

我对TypeScript还是个新手,所以我正在升级我的旧项目以利用它。
但是,我不确定在对某些数据调用Object.entries时如何保持正确的Type。
CodeSandbox example
例如:

    • 级别. 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])
    • 级别数据. 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;
    • 级别JSON. json:**
{
  "level_1": {
    "tiles": {
      "unpassable_tiles": {
        "rock": [[0, 0]],
        "tree": [[2, 0]]
      }
    }
  },
  "level_2": {
    "tiles": {
      "unpassable_tiles": {
        "rock": [[]],
        "tree": [[]]
      }
    }
  }
}

在上面的例子中,tiles表示数组的Array,每个数组都有两个数字。因此,[leftPos,topPos]都应该输入为number。然而,在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 []不应该被推断出来吗?
任何建议都将不胜感激。

62lalag4

62lalag41#

这与Why doesn't Object.keys() return a keyof type in TypeScript?之类的问题有关,这两个问题的答案都是TypeScript中的对象类型不是exact;对象类型的值允许编译器不知道的额外属性。2这允许接口和类继承,这是非常有用的。3但是它可能导致混乱。
例如,如果我有一个{name: string}类型的值nameHaver,我知道它有一个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());

如果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<[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;
}

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

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

这与我之前为entries手动编写的类型相同。如果在代码中使用ObjectEntries而不是Object.entries,它应该"修复"您的问题。但请记住,您所依赖的是这样一个事实,即您正在迭代其条目的对象没有未知的额外属性。如果出现这样的情况,即有人添加了非number[]类型更改为unpassable_tiles,则可能在运行时出现问题。
Playground代码链接

cbeh67ev

cbeh67ev2#

@jcalz出色的回答解释了为什么你要做的事情如此棘手。如果你想保持你的底层模式和JSON相同,他的方法可能会奏效。但是我要指出的是,你可以通过不同的数据结构来回避整个问题。我认为这会让你的开发者体验更好,也会让你更清楚地了解“你的数据是什么”。
您遇到的一个基本问题是,您试图将key: value对的Map视为某种不可通行的瓦片列表,但仅为了获得不可通行的瓦片类型而使用Object.entries本身就很笨拙和令人困惑。
为什么不将ImpassableTile定义为一个类型,将无法通过的tiles列表定义为该类型的数组呢?从概念上讲,这样可以更好地匹配数据实际表示的内容。它还可以完全避开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中的rocks或tree定义空数组,这些都是不存在的:

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

现在你可以很容易地Map你的不可通行的瓷砖,他们的类型,和相关的位置数据,同时保持完整的类型推理和安全性。

// 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
您可以进一步将这种理念扩展到如何构建您的Level及其接口。为什么不让levelJSON成为Level对象的数组,每个对象都有一个名称和一组tile?

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

相关问题