TypeScript: «Тип является универсальным и может индексироваться только для чтения. (2862)»

Представьте себе пример функции JS, для которой я хотел бы добавить типы:

const remap = (obj) => {
  const mapped = {};
  Object.keys(obj).forEach((key) => {
    mapped[key] = !!key;
  });

  return mapped;
};

Я пытаюсь добавить типы через общий (на этой игровой площадке TS), но постоянно сталкиваюсь с этой ошибкой:

Type 'Mapped<T>' is generic and can only be indexed for reading.(2862)
type Mapped<T> = {
  [K in keyof T]?: boolean;
};

const remap = <T extends Record<string, unknown>>(
  obj: T
) => {
  const mapped: Mapped<T> = {};
  Object.keys(obj).forEach((key) => {
    mapped[key] = !!key; // Type 'Mapped<T>' is generic and can only be indexed for reading.(2862)
  });

  return mapped;
};

Я хотел бы понять, почему TS не позволяет мне писать в этот объект универсального типа и есть ли другой способ обойти это. Я ожидаю, что TS поймет тип mapped и позволит мне писать ему, но этого не происходит.

Это единственный способ заставить TS принять это и использовать as при возвращении?

const remapWithAs = <T extends Record<string, unknown>>(
  obj: T
) => {
  const mapped: Record<string, boolean> = {};
  Object.keys(obj).forEach((key) => {
    mapped[key] = !!key;
  });

  return mapped as Mapped<T>; // Is this my only option?
};

Да, я думаю, вам нужно какое-то утверждение, чтобы это сработало. Если хотите, можете пропустить промежуточный шаг mapped: tsplay.dev/wXL21w

mochaccino 25.04.2024 22:37

Ваша проблема в том, что Object.keys() возвращает string[], а не (keyof T)[]. См. этот вопрос/ответ . Если вы утверждаете, что он возвращает keyof, как показано в ссылке на игровую площадку, то это сработает. Таким образом, утверждение необходимо, но только для того, чтобы избежать проблемы, когда Object.keys() может вернуть больше ключей, чем знает TS. Это полностью решает вопрос? Если да, то я напишу ответ (или свяжу подходящий дубликат). Если нет, то что мне не хватает?

jcalz 25.04.2024 22:38

@jcalz отвечает на мой вопрос и объясняет, почему он стоит. Спасибо! Совершенно понятно, почему это происходит, но меня смутило сообщение об ошибке; это заставило меня подумать, что я ничего не могу сделать, чтобы позволить мне проиндексировать его. (Также меня смутило отсутствие каких-либо ссылок на эту ошибку!) Основываясь на решении, я думаю, я ожидал бы сообщение, похожее на expression of type 'string' can't be used to index type Mapped<T>.

jjj_bbb 26.04.2024 01:55
Зод: сила проверки и преобразования данных
Зод: сила проверки и преобразования данных
Сегодня я хочу познакомить вас с библиотекой Zod и раскрыть некоторые ее особенности, например, возможности валидации и трансформации данных, а также...
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Как заставить Remix работать с Mantine и Cloudflare Pages/Workers
Мне нравится библиотека Mantine Component , но заставить ее работать без проблем с Remix бывает непросто.
Угловой продивер
Угловой продивер
Оригинал этой статьи на турецком языке. ChatGPT используется только для перевода на английский язык.
TypeScript против JavaScript
TypeScript против JavaScript
TypeScript vs JavaScript - в чем различия и какой из них выбрать?
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Синхронизация localStorage в масштабах всего приложения с помощью пользовательского реактивного хука useLocalStorage
Не все нужно хранить на стороне сервера. Иногда все, что вам нужно, это постоянное хранилище на стороне клиента для хранения уникальных для клиента...
Что такое ленивая загрузка в Angular и как ее применять
Что такое ленивая загрузка в Angular и как ее применять
Ленивая загрузка - это техника, используемая в Angular для повышения производительности приложения путем загрузки модулей только тогда, когда они...
2
3
685
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Основная причина ошибки заключается в том, что Object.keys(x) объявлен в библиотеке TS как возвращающий string[], а не что-то вроде (keyof typeof x)[]. Это намеренно; см. Почему Object.keys не возвращает тип keyof в TypeScript? . Поэтому, когда вы индексируете mapped[key], вы делаете это с ключом string, а не обязательно с ключом Mapped<T>. И поэтому технически небезопасно писать boolean к нему, потому что, возможно, вы пишете на ключ, о котором Mapped<T> не знает, и вы не можете быть уверены, что ключ примет boolean. Вам нужно будет либо заставить Mapped<T> просто быть {[k: string]: boolean} (что означает, что T не требуется), либо вам нужно утверждать, что то, что вы делаете, безопасно.

Обратите внимание, что TypeScript позволит вам прочитать boolean из mapped[key], хотя это также технически небезопасно:

Object.keys(obj).forEach((key) => {
  const test = mapped[key]; // boolean | undefined
});

Таков TypeScript. В любом случае, именно поэтому вы получаете сообщение об ошибке, что Mapped<T> можно индексировать только (с помощью string) для чтения. Раньше просто говорилось, что вы вообще не можете индексировать Mapped<T> с string, но поскольку это явно неправда, как показано выше, они изменили сообщение об ошибке на новую формулировку. См. microsoft/TypeScript#47357 для получения дополнительной информации.


В любом случае, обычный подход к утверждению — это сказать, что вы уверены, что Object.keys(obj) вернет (keyof T)[], несмотря на оговорки TypeScript о том, что, возможно, будут присутствовать другие ключи. Если вы это сделаете:

const remap = <T extends Record<string, unknown>>(
  initialState: T
) => {
  const mapped: Mapped<T> = {};
  (Object.keys(initialState) as (keyof T)[]).forEach(key => {
    mapped[key] = !!key; // okay
  });

  return mapped;
};

тогда все работает как написано. TypeScript с радостью допускает, что mapped[key] имеет тип Mapped<T>[keyof Mapped<T>], то есть boolean | undefined, и поэтому он принимает boolean.

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

Другие вопросы по теме