Kodokon kodokon.com

Продакшен-паттерны: брендированные типы, satisfies и .d.ts

Применяй экспертные паттерны, которые защищают реальную кодовую базу: имитация номинальности, проверка без расширения типа и внешние объявления.

11 мин · 3 вопросов

Открыть этот урок в Kodokon

Типизация в TypeScript структурная: два типа с одинаковой формой взаимозаменяемы, поэтому string с идентификатором пользователя неотличим от любой другой string. Брендирование имитирует номинальную типизацию, пересекая базовый тип с фантомным свойством, ключ которого - это unique symbol: ни у одного реального значения его нет, но компилятор теперь различает бренды.

TYPESCRIPT
declare const brand: unique symbol;

type Brand<T, Name extends string> =
  T & { readonly [brand]: Name };

type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;

function asUserId(raw: string): UserId {
  return raw as UserId;
}

declare function loadUser(id: UserId): void;

loadUser(asUserId("u_42")); // OK
// loadUser("u_42"); // error: bare string rejected
Имитация номинальности с нулевой стоимостью во время выполнения.

Оператор satisfies (TypeScript 4.9) закрывает конкретный пробел. Аннотация : T расширяет тип переменной до T и теряет выведенные литералы; as T отключает часть проверок. satisfies T проверяет, что выражение соответствует T, не трогая выведенный тип, который остаётся максимально точным. Это идеальный инструмент для конфигурационных объектов: полная проверка при сохранённой точности.

TYPESCRIPT
type RouteDef = { path: string; auth?: boolean };

const routes = {
  home: { path: "/" },
  admin: { path: "/admin", auth: true },
} satisfies Record<string, RouteDef>;

routes.admin.auth;
// literal type true: the inferred precision is
// preserved, and an unknown key would be rejected
satisfies: строгая проверка без расширения типа.

Файл .d.ts содержит исключительно внешние объявления: он описывает значения, которые будут существовать во время выполнения, не порождая ни строчки JavaScript. Чтобы расширить глобальную область из модуля, блок declare global обязателен - и спецификация разрешает его только в файле, который действительно является модулем, отсюда идиома export {} в начале файла.

TYPESCRIPT
export {};

declare global {
  interface Window {
    analytics: { track(event: string): void };
  }
}
env.d.ts: export {} превращает файл в модуль.

Для JavaScript-зависимости без опубликованных типов declare module создаёт внешний модуль: компилятор будет использовать твоё объявление при каждом импорте пакета. Следи за областью действия skipLibCheck: когда он включён, он игнорирует ошибки во всех файлах .d.ts, включая твои; поэтому проверяй свои объявления тестами типов, размещёнными в обычных файлах .ts.

TYPESCRIPT
declare module "legacy-lib" {
  export interface InitOptions {
    debug?: boolean;
  }
  export function init(options?: InitOptions): void;
}
legacy-lib.d.ts: типизация пакета без опубликованных типов.

Проверка знаний

Убедись, что запомнил ключевые моменты этого урока.

  1. В чём разница между satisfies T и аннотацией : T?
    • satisfies расширяет тип выражения до T
    • satisfies проверяет соответствие T, сохраняя более точный выведенный тип
    • satisfies отключает проверки, как as
    • Никакой: обе формы эквивалентны
  2. Что во время выполнения содержит брендированное значение, построенное через Brand<string, ...>?
    • Строку вместе со свойством-символом
    • Обычную строку: бренд существует только во время компиляции
    • Объект, оборачивающий исходную строку
  3. Зачем добавлять export {} в начало файла .d.ts, который использует declare global?
    • Чтобы экспортировать глобальные типы в другие файлы
    • Чтобы превратить файл в модуль - обязательное условие для declare global
    • Чтобы автоматически включить строгий режим
    • Это чисто декоративно