typeof и keyof
typeofполучает тип из значения,keyofполучает union ключей из типа. Вместе с indexed access types создают мощные комбинации.
Зачем нужно
typeof— получить тип переменной, объекта или функции без дублированияkeyof— получить union всех ключей типа для типобезопасного доступа к свойствам- Indexed access types — получить тип конкретного свойства:
T[K] - Эти операторы — основа для Pick, Omit, Record и других утилит
Где используется
- Извлечение типов из констант и конфигов:
typeof config - Типобезопасный доступ к свойствам:
keyof T+T[K] - Генерация типов из runtime-значений
- Mapped types:
[K in keyof T]
Предпосылки
- Примитивные типы, Интерфейсы — базовые типы
- Generics — generics с keyof constraints
typeof в контексте типов
В JavaScript typeof возвращает строку ("string", "number", ...). В TypeScript typeof в позиции типа возвращает тип TypeScript:
const user = {
id: 1,
name: "Alice",
email: "alice@example.com",
};
// typeof в контексте типа — получаем TypeScript-тип
type User = typeof user;
// { id: number; name: string; email: string }
// Без дублирования!
function createUser(data: typeof user): void {
// ...
}
// typeof для функций
function add(a: number, b: number): number {
return a + b;
}
type AddFn = typeof add;
// (a: number, b: number) => number
// typeof для массивов
const colors = ["red", "green", "blue"];
type Colors = typeof colors; // string
// typeof + as const — получаем точный тип
const colors = ["red", "green", "blue"] as const;
type Colors = typeof colors; // readonly ["red", "green", "blue"]
typeof vs value-level typeof
// Value-level typeof — возвращает строку в рантайме
const x = "hello";
console.log(typeof x); // "string" (runtime)
// Type-level typeof — возвращает TypeScript-тип
type X = typeof x; // string (если let) или "hello" (если const)
// Разница:
if (typeof x === "string") {
// Runtime проверка — type guard
}
type T = typeof x; // Compile-time тип — другое значение typeof
keyof — ключи типа
keyof T возвращает union всех ключей типа T:
interface User {
id: number;
name: string;
email: string;
age: number;
}
type UserKeys = keyof User;
// "id" | "name" | "email" | "age"
// Типобезопасный доступ к свойствам
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user: User = { id: 1, name: "Alice", email: "a@b.com", age: 30 };
const name = getProperty(user, "name"); // string
const age = getProperty(user, "age"); // number
const bad = getProperty(user, "password"); // Ошибка! "password" не в keyof User
keyof с index signatures
interface Dictionary {
[key: string]: unknown;
}
type DictKeys = keyof Dictionary; // string | number
// number включён потому что JS преобразует числовые ключи в строки
interface NumberMap {
[key: number]: string;
}
type NumKeys = keyof NumberMap; // number
keyof с enum и as const
// keyof typeof для объектов
const STATUS = {
active: "ACTIVE",
inactive: "INACTIVE",
banned: "BANNED",
} as const;
type StatusKey = keyof typeof STATUS;
// "active" | "inactive" | "banned"
type StatusValue = (typeof STATUS)[StatusKey];
// "ACTIVE" | "INACTIVE" | "BANNED"
Indexed Access Types (T[K])
Получение типа свойства по ключу:
interface User {
id: number;
name: string;
address: {
city: string;
zip: string;
};
tags: string;
}
// Тип конкретного свойства
type UserId = User["id"]; // number
type UserName = User["name"]; // string
type UserAddress = User["address"]; // { city: string; zip: string }
// Вложенный доступ
type City = User["address"]["city"]; // string
// Union ключей — union значений
type IdOrName = User["id" | "name"]; // number | string
// Все значения объекта
type UserValues = User[keyof User];
// number | string | { city: string; zip: string } | string
// Тип элемента массива
type Tag = User["tags"][number]; // string
// number как индекс массива/кортежа
const tuple = [1, "hello", true] as const;
type TupleElement = (typeof tuple)[number]; // 1 | "hello" | true
type First = (typeof tuple)[0]; // 1
type Second = (typeof tuple)[1]; // "hello"
Комбинации typeof + keyof
// Получить ключи объекта-значения
const config = {
apiUrl: "https://api.example.com",
timeout: 5000,
debug: false,
};
type ConfigKey = keyof typeof config;
// "apiUrl" | "timeout" | "debug"
type ConfigValue = (typeof config)[keyof typeof config];
// string | number | boolean
// Типобезопасная функция для конфига
function getConfig<K extends keyof typeof config>(key: K): (typeof config)[K] {
return config[key];
}
const url = getConfig("apiUrl"); // string
const timeout = getConfig("timeout"); // number
const debug = getConfig("debug"); // boolean
// Для enum-подобных объектов
const HttpStatus = {
OK: 200,
NotFound: 404,
InternalError: 500,
} as const;
type StatusName = keyof typeof HttpStatus; // "OK" | "NotFound" | "InternalError"
type StatusCode = (typeof HttpStatus)[StatusName]; // 200 | 404 | 500
Продвинутые паттерны
Типобезопасный EventEmitter
interface EventMap {
click: { x: number; y: number };
focus: { element: string };
resize: { width: number; height: number };
}
class TypedEmitter<Events extends Record<string, unknown>> {
on<K extends keyof Events>(
event: K,
callback: (data: Events[K]) => void
): void {
// ...
}
emit<K extends keyof Events>(event: K, data: Events[K]): void {
// ...
}
}
const emitter = new TypedEmitter<EventMap>;
emitter.on("click", (data) => {
console.log(data.x, data.y); // Типобезопасно!
});
emitter.emit("focus", { element: "input" }); // OK
emitter.emit("focus", { x: 1 }); // Ошибка!
Типобезопасный маппинг объектов
function mapValues<T extends Record<string, unknown>, U>(
obj: T,
fn: (value: T[keyof T], key: keyof T) => U
): { [K in keyof T]: U } {
const result = {} as { [K in keyof T]: U };
for (const key in obj) {
result[key] = fn(obj[key], key);
}
return result;
}
const lengths = mapValues({ a: "hello", b: "world" }, (v) =>
String(v).length
);
// { a: number; b: number }
Извлечение типов из сложных структур
// Тип из промиса
type Unwrap<T> = T extends Promise<infer U> ? U : T;
// Тип элемента массива
type ArrayElement<T> = T extends (infer E) ? E : never;
// Тип возвращаемого значения
type Return<T> = T extends (...args: any) => infer R ? R : never;
// Комбинация
async function fetchUsers: Promise<User> { /* ... */ }
type Result = Unwrap<ReturnType<typeof fetchUsers>>;
// User
type SingleUser = ArrayElement<Result>;
// User
Частые ошибки
- typeof на типе, а не на значении
interface User { name: string; }
type T = typeof User; // Ошибка если User — interface (не существует в рантайме)
// typeof работает только с runtime-значениями
const user = { name: "Alice" };
type T = typeof user; // OK
- keyof any — возвращает
string | number | symbol
type K = keyof any; // string | number | symbol
// Используется в Record<K extends keyof any, V>
- Indexed access с несуществующим ключом
interface User { name: string; }
type Bad = User["age"]; // Ошибка! Property 'age' does not exist
- Забыть
as constпри typeof — без него тип расширяется
const config = { mode: "production" };
type Mode = (typeof config)["mode"]; // string (не "production"!)
const config = { mode: "production" } as const;
type Mode = (typeof config)["mode"]; // "production"
- keyof union vs keyof intersection
type A = { a: string; common: number };
type B = { b: string; common: number };
type UnionKeys = keyof (A | B); // "common" — только общие ключи
type IntersectionKeys = keyof (A & B); // "a" | "b" | "common" — все ключи
Практика
- Используйте
typeofдля извлечения типа из объекта-конфигурации - Напишите
getProperty<T, K extends keyof T>(obj: T, key: K): T[K] - Создайте тип для всех значений enum-подобного объекта через
(typeof obj)[keyof typeof obj] - Реализуйте typed event emitter с
keyofи indexed access - Извлеките тип из вложенного свойства через цепочку indexed access:
T["a"]["b"]["c"]
Связанные темы
- Type guards — typeof в value context
- Mapped types —
[K in keyof T] - Generics — keyof в constraints
- Utility types — Pick, Omit используют keyof