Представьте себе пример функции 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?
};
Ваша проблема в том, что Object.keys()
возвращает string[]
, а не (keyof T)[]
. См. этот вопрос/ответ . Если вы утверждаете, что он возвращает keyof
, как показано в ссылке на игровую площадку, то это сработает. Таким образом, утверждение необходимо, но только для того, чтобы избежать проблемы, когда Object.keys()
может вернуть больше ключей, чем знает TS. Это полностью решает вопрос? Если да, то я напишу ответ (или свяжу подходящий дубликат). Если нет, то что мне не хватает?
@jcalz отвечает на мой вопрос и объясняет, почему он стоит. Спасибо! Совершенно понятно, почему это происходит, но меня смутило сообщение об ошибке; это заставило меня подумать, что я ничего не могу сделать, чтобы позволить мне проиндексировать его. (Также меня смутило отсутствие каких-либо ссылок на эту ошибку!) Основываясь на решении, я думаю, я ожидал бы сообщение, похожее на expression of type 'string' can't be used to index type Mapped<T>
.
Основная причина ошибки заключается в том, что 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
.
Да, я думаю, вам нужно какое-то утверждение, чтобы это сработало. Если хотите, можете пропустить промежуточный шаг
mapped
: tsplay.dev/wXL21w