Вы замечали, что Object.keys и Object.entries в TypeScript работают не совсем интуитивно? Они возвращают не совсем то, что можно было бы ожидать, даже с readonly-объектом.
Давайте разберёмся.
Возьмём следующий объект с константным утверждением (as const), чтобы TypeScript использовал наиболее конкретный тип выражения и сделал свойства readonly:
const data = {
a: 'value-a',
b: 'value-b',
c: 'value-c',
} as const;
Можно ожидать, что Object.values вернёт литеральные значения объекта — и это действительно так:
const values = Object.values(data);
// ^? const values: ("value-a" | "value-b" | "value-c")[]
Но что насчёт Object.keys и Object.entries?
Object.keys
При вызове Object.keys с нашим объектом возвращается string[]:
const keys = Object.keys(data);
// ^? const keys: string[]
И это сделано намеренно! Object.keys всегда возвращает string[]:
/**
* Возвращает имена перечисляемых строковых свойств и методов объекта.
* @param o - Объект, содержащий свойства и методы. Это может быть как созданный вами объект, так и существующий DOM-объект.
*/
keys(o: {}): string[];
Это связано с тем, что типы в TypeScript намеренно остаются открытыми, поэтому он не всегда может гарантировать, что в вашем объекте нет дополнительных свойств, даже если он определён с as const. 😥
Возможно, ситуация изменится, когда появится Record (глубоко неизменяемая структура, похожая на объект). 🤞
Решение
К счастью, TypeScript предоставляет оператор keyof, который возвращает тип ключей заданного типа:
⚠ Предупреждение
Это работает только если объект неизменяем и не содержит лишних свойств.
// Тип дополнен `& {}`, чтобы упростить отображение (иначе показывается просто `Keys`)
type Keys = (keyof typeof data)[] & {};
// ^? type Keys = ("a" | "b" | "c")[]
Затем можно привести результат Object.keys к нашему типу:
const typedKeys = Object.keys(data) as Keys;
// ^? const typedKeys: ("a" | "b" | "c")[]
Или в сокращённой форме:
const typedKeys = Object.keys(data) as (keyof typeof data)[];
// ^? const typedKeys: ("a" | "b" | "c")[]
Можно пойти дальше и создать обобщённую функцию:
function keysFromObject<T extends object>(object: T): (keyof T)[] {
return Object.keys(object) as (keyof T)[];
}
const typedKeys = keysFromObject(data);
// ^? const typedKeys: ("a" | "b" | "c")[]
Object.entries
То же самое касается Object.entries.
value определяется корректно, но key имеет тип string:
const entries = Object.entries(data).map(
([key, value]) => [key, value],
// ^? (parameter) key: string
);
Решение
Можно использовать keyof вместе с обобщённым типом, чтобы захватить ключи объекта, а затем привести результат Object.entries:
type Entries<T> = {
[K in keyof T]: [K, T[K]];
}[keyof T][];
const typedEntries = (Object.entries(data) as Entries<typeof data>).map(
([key, value]) => [key, value],
// ^? (parameter) key: "a" | "b" | "c"
);
Также можно воспользоваться типом Entries из библиотеки type-fest:
import type { Entries } from 'type-fest';
const typedEntries = (Object.entries(data) as Entries<typeof data>).map(
([key, value]) => [key, value],
// ^? (parameter) key: "a" | "b" | "c"
);
Или создать свою обобщённую функцию:
function entriesFromObject<T extends object>(object: T): Entries<T> {
return Object.entries(object) as Entries<T>;
}
const typedEntries2 = entriesFromObject(data).map(
([key, value]) => [key, value],
// ^? (parameter) key: "a" | "b" | "c"
);
