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]

Предпосылки

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

Частые ошибки

  1. typeof на типе, а не на значении
interface User { name: string; }
type T = typeof User; // Ошибка если User — interface (не существует в рантайме)

// typeof работает только с runtime-значениями
const user = { name: "Alice" };
type T = typeof user; // OK
  1. keyof any — возвращает string | number | symbol
type K = keyof any; // string | number | symbol
// Используется в Record<K extends keyof any, V>
  1. Indexed access с несуществующим ключом
interface User { name: string; }
type Bad = User["age"]; // Ошибка! Property 'age' does not exist
  1. Забыть 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"
  1. 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" — все ключи

Практика

  1. Используйте typeof для извлечения типа из объекта-конфигурации
  2. Напишите getProperty<T, K extends keyof T>(obj: T, key: K): T[K]
  3. Создайте тип для всех значений enum-подобного объекта через (typeof obj)[keyof typeof obj]
  4. Реализуйте typed event emitter с keyof и indexed access
  5. Извлеките тип из вложенного свойства через цепочку indexed access: T["a"]["b"]["c"]

Связанные темы

Ресурсы