Сохранить тип при использовании Object.entries - PullRequest
0 голосов
/ 28 мая 2020

Я новичок в TypeScript, поэтому сейчас обновляю свои старые проекты, чтобы использовать его.

Однако я не уверен, как сохранить правильный тип при вызове Object. записи о некоторых данных.

Пример CodeSandbox

Например:

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;

level JSON. json:

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

В случае В приведенном выше фрагменте плитки представляют собой массив массивов, каждый с двумя числами. Следовательно, [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}
        />
      ))

Но разве не следует выводить число [] в любом случае?

Любые советы будут признательны.

Ответы [ 2 ]

1 голос
/ 28 мая 2020

Это связано с такими вопросами, как Почему Object.keys() не возвращает тип keyof в TypeScript? . Ответ на оба вопроса заключается в том, что типы объектов в TypeScript не являются точными ; Допускаются значения типов объектов для дополнительных свойств, о которых компилятор не знает. Это позволяет наследовать интерфейс и класс, что очень полезно. Но это может привести к путанице.

Например, если у меня есть значение nameHaver типа {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()); 

Что, если 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 - это утверждение типа , которое подавляет обычную жалобу на Object.entries(). Тип Entries<T> - это отображаемый тип , который мы немедленно ищем , чтобы произвести объединение известных записей:

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

Это тот же тип I вручную писал раньше для entries. Если вы используете ObjectEntries вместо Object.entries в коде, это должно «исправить» вашу проблему. Но имейте в виду, что вы полагаетесь на тот факт, что объект, записи которого вы повторяете, не имеет неизвестных дополнительных свойств. Если когда-нибудь случится так, что кто-то добавит дополнительное свойство не- number[] типа к unpassable_tiles, у вас могут возникнуть проблемы во время выполнения.


Хорошо, надеюсь, это поможет; удачи!

Детская площадка ссылка на код

0 голосов
/ 28 мая 2020

Отличный ответ @ 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;

Чтобы должным образом соответствовать новому интерфейсу, вам необходимо изменить уровень JSON. json тоже. Но обратите внимание, что он намного чище, и вам не нужно определять пустые массивы для камней или деревьев на уровне_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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...