$ sudo teach IT
МОДУЛЬ 7 · УРОК 3

Литеральные типы (Literal Types)

Как создавать типы, которые соответствуют конкретным значениям, и использовать их для максимально точной типизации кода

⏱ ~30 мин
📊 Средний уровень
🧩 Literal Types
🔤 TypeScript

Привет! На предыдущем уроке мы изучили типы-пересечения (Intersection Types) — способ комбинировать несколько типов в один. Сегодня мы погрузимся в мир литеральных типов (Literal Types) — одного из самых мощных и элегантных инструментов TypeScript. Если раньше вы работали с типами как с «контейнерами» для значений (например, string — это любая строка), то теперь мы научимся создавать типы, которые соответствуют конкретным значениям!

Представьте, что вы разрабатываете мобильное приложение для доставки еды. У вас есть статус заказа: «создан», «в обработке», «доставлен», «отменён». Вы можете использовать тип string для статуса — но тогда ничто не помешает вам написать "abcxyz" в качестве статуса. А если использовать литеральный тип — TypeScript будет знать, что статус может быть только одним из четырёх допустимых значений!

Литеральные типы — это фундамент для многих продвинутых паттернов в TypeScript: union-типы с ограничением, discriminated unions, template literal types и многое другое. Давайте разберёмся!

1

Что такое литеральный тип?

Литеральный тип — это тип, который соответствует конкретному значению. В отличие от базовых типов (string, number, boolean), которые описывают класс значений, литеральный тип описывает одно конкретное значение.

Синтаксис предельно прост — просто используйте значение как тип:

// Строка "hello" как тип
type Greeting = "hello";

// Число 42 как тип
type Answer = 42;

// Boolean true как тип
type IsReady = true;

// Присваивание — работает!
const msg: Greeting = "hello";    // ✓ OK
const num: Answer = 42;           // ✓ OK
const ready: IsReady = true;      // ✓ OK

// Но другое значение — ОШИБКА!
// const msg2: Greeting = "hi";   // ✗ Ошибка!
// const num2: Answer = 100;      // ✗ Ошибка!
// const ready2: IsReady = false; // ✗ Ошибка!

Как вы видите, тип Greeting может принимать только значение "hello". Любое другое значение вызовет ошибку компиляции. Это как строгое ограничение — вы говорите TypeScript: «здесь может быть только это конкретное значение, ничего больше».

Аналогия из жизни: представьте, что вы покупаете билет в кино. Билет имеет конкретное место — «ряд 5, место 12». Это и есть литеральный тип: не «любое место в зале» (это было бы как тип Seat), а конкретное место (это литеральный тип "row5-seat12").

📝 Важно: Литеральный тип сам по себе — это не «новая фича», а способ использования уже существующих типов. TypeScript автоматически «сужает» (narrowing) тип до литерального при присваивании константного значения. Например, const x = "hello" автоматически получает тип "hello", а не string.

2

Строковые литеральные типы (String Literal Types)

Строковые литеральные типы — самый распространённый вид литеральных типов. Они позволяют ограничить строковое значение конкретным набором строк. Это особенно полезно для статусов, ролей, направлений и других перечисляемых значений.

// Определяем строковый литеральный тип
type OrderStatus = "created" | "processing" | "shipped" | "delivered" | "cancelled";

// Используем его
function getStatus(status: OrderStatus): string {
  switch (status) {
    case "created":
      return "Заказ создан";
    case "processing":
      return "Заказ обрабатывается";
    case "shipped":
      return "Заказ отправлен";
    case "delivered":
      return "Заказ доставлен";
    case "cancelled":
      return "Заказ отменён";
  }
}

// ✓ Работает — значение из допустимого набора
const s1: OrderStatus = "created";
const s2: OrderStatus = "shipped";

// ✗ Ошибка — "pending" нет в списке допустимых значений
// const s3: OrderStatus = "pending";

Обратите внимание: тип OrderStatus — это union из строковых литералов. TypeScript понимает, что значение может быть одним из перечисленных строк. Это гораздо безопаснее, чем просто string.

Ещё один пример — HTTP методы:

type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH";

function makeRequest(method: HttpMethod, url: string) {
  console.log(`Выполняю ${method} запрос к ${url}`);
}

makeRequest("GET", "/api/users");     // ✓ OK
makeRequest("POST", "/api/users");    // ✓ OK
// makeRequest("get", "/api/users");  // ✗ Ошибка! "get" ≠ "GET"
// makeRequest("HEAD", "/api/users"); // ✗ Ошибка! "HEAD" не в списке

📝 Важно: Строковые литеральные типы регистрозависимы! "GET" и "get" — это разные типы. Если вам нужно игнорировать регистр, используйте Uppercase<T> или другие utility types.

Пример с ролями пользователей:

type UserRole = "admin" | "editor" | "viewer";

function checkAccess(role: UserRole, resource: string): boolean {
  if (role === "admin") {
    return true; // Админ имеет доступ ко всему
  }
  if (role === "editor") {
    return resource !== "settings"; // Редактор не может менять настройки
  }
  // role === "viewer"
  return false; // Зритель только просматривает
}

// Автоматическое завершение (autocomplete) в IDE!
const userRole: UserRole = "ad" // ← IDE предложит "admin"

💡 Совет: Одно из главных преимуществ литеральных типов — автодополнение в IDE. Когда вы пишете const role: UserRole = ", TypeScript подсказывает все возможные значения. Этосильно улучшает DX (developer experience) и снижает количество ошибок.

Практический пример — системные коды ошибок:

type ErrorCode = "NOT_FOUND" | "UNAUTHORIZED" | "FORBIDDEN" | "INTERNAL_ERROR";

interface ApiError {
  code: ErrorCode;
  message: string;
  details?: Record<string, unknown>;
}

function handleError(error: ApiError): string {
  switch (error.code) {
    case "NOT_FOUND":
      return `Ресурс не найден: ${error.message}`;
    case "UNAUTHORIZED":
      return "Пожалуйста, войдите в систему";
    case "FORBIDDEN":
      return "У вас нет прав для выполнения этого действия";
    case "INTERNAL_ERROR":
      return "Произошла внутренняя ошибка сервера";
  }
}

const err: ApiError = {
  code: "NOT_FOUND",
  message: "Пользователь с ID 42 не найден"
};

console.log(handleError(err));
3

Числовые литеральные типы (Number Literal Types)

Числовые литеральные типы работают аналогично строковым, но ограничивают значение конкретным числом. Это полезно для магических чисел, индексов, кодов HTTP и других ситуаций, когда значение должно быть точно определено.

// Конкретное число как тип
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;

function rollDice(): DiceRoll {
  return Math.floor(Math.random() * 6) + 1 as DiceRoll;
}

const result: DiceRoll = 3;  // ✓ OK
// const bad: DiceRoll = 7;  // ✗ Ошибка! 7 не в диапазоне

// HTTP статус-коды
type SuccessCode = 200 | 201 | 204;
type ErrorCode4xx = 400 | 401 | 403 | 404 | 422;
type ErrorCode5xx = 500 | 502 | 503;

type HttpStatusCode = SuccessCode | ErrorCode4xx | ErrorCode5xx;

function getStatusMessage(code: HttpStatusCode): string {
  switch (code) {
    case 200: return "OK";
    case 201: return "Created";
    case 204: return "No Content";
    case 400: return "Bad Request";
    case 401: return "Unauthorized";
    case 403: return "Forbidden";
    case 404: return "Not Found";
    case 422: return "Unprocessable Entity";
    case 500: return "Internal Server Error";
    case 502: return "Bad Gateway";
    case 503: return "Service Unavailable";
  }
}

// Теперь коды сгруппированы по смыслу!
const status200: SuccessCode = 200;      // ✓ OK
const error404: ErrorCode4xx = 404;      // ✓ OK
// const bad: SuccessCode = 404;          // ✗ Ошибка! 404 — это ErrorCode4xx

Пример с позициями в массиве:

// Позиция в двумерном массиве (матрице)
type Row = 0 | 1 | 2 | 3 | 4;
type Col = 0 | 1 | 2 | 3 | 4;

interface CellPosition {
  row: Row;
  col: Col;
}

function getCell(position: CellPosition): string {
  return `Ячейка [${position.row},${position.col}]`;
}

const pos: CellPosition = { row: 2, col: 3 };
console.log(getCell(pos)); // "Ячейка [2,3]"

// Ошибка — выходит за границы
// const badPos: CellPosition = { row: 5, col: 0 };

💡 Совет: Числовые литеральные типы отлично подходят для устранения магических чисел. Вместо того чтобы писать if (status === 200), используйте именованный тип type SuccessCode = 200. Код становится самодокументирующимся!

Пример с шахматными координатами:

type ChessFile = "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h";
type ChessRank = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
type ChessSquare = `${ChessFile}${ChessRank}`; // Строка "a1" | "a2" | ... | "h8"

function getSquareColor(square: ChessSquare): "white" | "black" {
  const file = square[0] as string;
  const rank = parseInt(square[1]);
  const fileIndex = file.charCodeAt(0) - 97; // a=0, b=1, ..., h=7
  return (fileIndex + rank) % 2 === 0 ? "white" : "black";
}

console.log(getSquareColor("a1")); // "white"
console.log(getSquareColor("h8")); // "white"
console.log(getSquareColor("a8")); // "black"
// getSquareColor("i9"); // ✗ Ошибка!
4

Булевы литеральные типы (Boolean Literal Types)

Булевы литеральные типы — это типы true или false. Они используются реже строковых и числовых, но имеют важные применения, особенно в conjunction с generics и conditional types.

// Булев литеральный тип
type IsAdmin = true;
type IsEnabled = boolean; // Это.union true | false

interface Config {
  readonly: true;          // Только true — свойство только для чтения
  debug: boolean;          // true или false
  verbose: false;          // Только false — вывод отключён
}

const config: Config = {
  readonly: true,      // ✓ OK
  debug: false,        // ✓ OK
  verbose: false,      // ✓ OK
};

// config.readonly = false;  // ✗ Ошибка! readonly: true
// config.verbose = true;    // ✗ Ошибка! verbose: false

Булевы литеральные типы особенно полезны с условными типами и перегрузками функций:

// Функция, которая возвращает разные типы в зависимости от флага
function createResponse<T>(data: T, isArray: true): T[];
function createResponse<T>(data: T, isArray: false): T;
function createResponse<T>(data: T, isArray: boolean): T | T[] {
  return isArray ? [data] : data;
}

// TypeScript знает тип возвращаемого значения!
const single = createResponse({ id: 1 }, false);
//    ^? тип: { id: number }

const array = createResponse({ id: 1 }, true);
//    ^? тип: { id: number }[]

// Ошибка — передаём boolean вместо литерала
// createResponse({ id: 1 }, someVariable); // ✗ Ошибка!

⚠️ Предупреждение: Не путайте литеральный тип true с типом boolean. Тип boolean — это true | false, а литеральный true — это только одно значение. Используйте литеральные типы, когда значение должно быть фиксированным.

5

Утверждение "as const" (Const Assertions)

Утверждение as const — это способ сказать TypeScript: «сделай это значение максимально конкретным». Оно превращает все строки в строковые литералы, все числа — в числовые литералы, и делает все свойства readonly.

// БЕЗ as const — TypeScript сужает типы, но не до литералов
const config1 = {
  method: "GET",
  timeout: 3000,
};
// Тип: { method: string; timeout: number }

// С as const — типы максимально конкретные
const config2 = {
  method: "GET",
  timeout: 3000,
} as const;
// Тип: { readonly method: "GET"; readonly timeout: 3000 }

// Теперь нельзя изменить значения
// config2.method = "POST";  // ✗ Ошибка! readonly
// config2.timeout = 5000;   // ✗ Ошибка! readonly

Разница между as const и обычным const:

// const — делает переменную неизменяемой (привязка)
const x = 10;
// x = 20; // ✗ Ошибка! const

// as const — делает тип максимально узким (литеральным)
const arr1 = [1, 2, 3];
// Тип: number[] — можно добавлять/менять элементы
arr1.push(4); // ✓ OK

const arr2 = [1, 2, 3] as const;
// Тип: readonly [1, 2, 3] — нельзя менять
// arr2.push(4); // ✗ Ошибка!
// arr2[0] = 99; // ✗ Ошибка!

as const на массивах создаёт tuple-подобные типы:

// Массив без as const
const colors1 = ["red", "green", "blue"];
// Тип: string[] — TypeScript не знает конкретные значения

// Массив с as const
const colors2 = ["red", "green", "blue"] as const;
// Тип: readonly ["red", "green", "blue"]

// Теперь TypeScript знает длину и содержимое!
type Colors = typeof colors2;
// Тип: readonly ["red", "green", "blue"]

// Можно получить литеральные типы из кортежа
type FirstColor = Colors[0]; // "red"
type SecondColor = Colors[1]; // "green"
type ThirdColor = Colors[2]; // "blue"

// Или получить union всех значений
type AnyColor = Colors[number]; // "red" | "green" | "blue"

💡 Совет: as const — это «суперсила» для создания константных объектов и массивов. Используйте его для конфигураций, списков значений, маппингов и любых данных, которые не должны меняться. Это безопаснее, чем просто const, потому что garantирует и неизменяемость, и типизацию.

Практический пример — конфигурация приложения:

// Конфигурация с as const
const AppConfig = {
  api: {
    baseUrl: "https://api.example.com",
    version: "v2",
    timeout: 5000,
  },
  features: {
    darkMode: true,
    notifications: false,
    analytics: true,
  },
  limits: {
    maxUploadSize: 10 * 1024 * 1024, // 10 MB
    maxFileSize: 5 * 1024 * 1024,     // 5 MB
  },
} as const;

// TypeScript знает типы всех значений!
type ApiConfig = typeof AppConfig.api;
// Тип: { readonly baseUrl: "https://api.example.com"; readonly version: "v2"; readonly timeout: 5000 }

// Нельзя изменить — всё readonly
// AppConfig.api.baseUrl = "https://other.com"; // ✗ Ошибка!
6

Шаблонные литеральные типы (Template Literal Types)

Шаблонные литеральные типы (Template Literal Types) — это одна из самых мощных фич TypeScript 4.1+. Они позволяют создавать строки, используя шаблоны, аналогично шаблонным строкам в JavaScript, но на уровне типов!

// Простой шаблонный литеральный тип
type Name = "Alice" | "Bob";
type Greeting = `Hello, ${Name}!`;
// Тип: "Hello, Alice!" | "Hello, Bob!"

const g1: Greeting = "Hello, Alice!";  // ✓ OK
const g2: Greeting = "Hello, Bob!";    // ✓ OK
// const g3: Greeting = "Hello, Charlie!"; // ✗ Ошибка!

Комбинирование нескольких литеральных типов в шаблоне создаёт декартово произведение:

type Color = "red" | "blue";
type Size = "small" | "large";
type CSSClassName = `${Color}-${Size}`;
// Тип: "red-small" | "red-large" | "blue-small" | "blue-large"

// 4 варианта из 2 × 2!

const className1: CSSClassName = "red-small";   // ✓ OK
const className2: CSSClassName = "blue-large";  // ✓ OK
// const className3: CSSClassName = "green-small"; // ✗ Ошибка!

Создание типизированных CSS-классов:

type TailwindSpacing = "0" | "1" | "2" | "3" | "4" | "6" | "8" | "12" | "16";
type Direction = "t" | "b" | "l" | "r" | "x" | "y";
type PaddingClass = `p-${TailwindSpacing}`;
type MarginClass = `m-${Direction}-${TailwindSpacing}`;

// Padding: "p-0" | "p-1" | "p-2" | ... | "p-16"
// Margin: "mt-0" | "mb-1" | "ml-2" | ... | "my-16"

const p: PaddingClass = "p-4";     // ✓ OK
const m: MarginClass = "mx-8";     // ✓ OK
// const bad: PaddingClass = "p-20"; // ✗ Ошибка! "p-20" нет в списке

Шаблонные литеральные типы с встроенными utility types:

type UppercaseColor = Uppercase<"red" | "blue" | "green">;
// "RED" | "BLUE" | "GREEN"

type LowercaseColor = Lowercase<"RED" | "BLUE" | "GREEN">;
// "red" | "blue" | "green"

type CapitalizedColor = Capitalize<"red" | "blue" | "green">;
// "Red" | "Blue" | "Green"

type UncapitalizedColor = Uncapitalize<"Red" | "Blue" | "Green">;
// "red" | "blue" | "green"

// Комбинируем
type EventName = "click" | "hover" | "focus";
type EventCallback = `on${Capitalize<EventName>}`;
// "onClick" | "onHover" | "onFocus"

💡 Совет: Шаблонные литеральные типы — это самая мощная фича для работы со строками на уровне типов. Они позволяют создавать типизированные API-эндпоинты, CSS-классы, имена событий и многое другое. Освойте их — и вы сможете создавать невероятно точные типы!

Практический пример — типизированные API-эндпоинты:

type ApiVersion = "v1" | "v2" | "v3";
type Resource = "users" | "posts" | "comments";
type Endpoint = `/${ApiVersion}/${Resource}`;
// "/v1/users" | "/v1/posts" | ... | "/v3/comments" (18 вариантов)

type Method = "GET" | "POST" | "PUT" | "DELETE";
type Route = `${Method} /${ApiVersion}/${Resource}`;
// "GET /v1/users" | "POST /v1/users" | ... | "DELETE /v3/comments"

function fetchApi<T>(route: Route): Promise<T> {
  const [method, path] = route.split(" ");
  return fetch(path, { method }).then(r => r.json());
}

// TypeScript проверяет маршрут!
fetchApi("GET /v2/users");    // ✓ OK
fetchApi("POST /v1/posts");   // ✓ OK
// fetchApi("GET /v4/users");  // ✗ Ошибка! v4 не существует
// fetchApi("PATCH /v1/users"); // ✗ Ошибка! PATCH не в списке
7

Enum vs Литеральные типы: что выбрать?

Часто возникает вопрос: когда использовать enum, а когда — union литеральных типов? Давайте сравним!

// Вариант 1: Enum
enum Direction {
  Up = "UP",
  Down = "DOWN",
  Left = "LEFT",
  Right = "RIGHT",
}

// Вариант 2: Union литеральных типов
type Direction2 = "UP" | "DOWN" | "LEFT" | "RIGHT";

// Вариант 3: Union литеральных типов с объектом
const Direction3 = {
  Up: "UP",
  Down: "DOWN",
  Left: "LEFT",
  Right: "RIGHT",
} as const;

type Direction3 = typeof Direction3[keyof typeof Direction3];
// Тип: "UP" | "DOWN" | "LEFT" | "RIGHT"

Сравнительная таблица:

Критерий Enum Literal Union
Размер JS-кода Больше (генерирует объект) Меньше (стирается)
Tree-shaking Плохо Отлично
Autocomplete Отлично Отлично
Реверс-маппинг Да Нет
Runtime-значения Доступны Только через объект
Вычисляемые значения Да Нет

Когда использовать что:

// ✓ Используйте LITERAL UNION, когда:
// - Нужен минимальный размер бандла
// - Значения — просто строки/числа
// - Нужен tree-shaking

type Status = "active" | "inactive" | "pending";

// ✓ Используйте ENUM, когда:
// - Нужны вычисляемые значения
// - Нужен реверс-маппинг (значение → ключ)
// - Нужны runtime-значения для логики

enum HttpStatus {
  OK = 200,
  NotFound = 404,
  Error = 500,
}

// Реверс-маппинг: 404 → "NotFound"
console.log(HttpStatus[404]); // "NotFound"

⚠️ Предупреждение: Многие эксперты рекомендуют избегать enum в современном TypeScript и использовать union литеральных типов с as const. Enum генерирует лишний JavaScript-код, плохо tree-shакается, и его поведение может быть неожиданным. Если вам не нужен реверс-маппинг или вычисляемые значения — предпочитайте литеральные типы.

Альтернатива enum с as const:

// Объект с as const + typeof = лучшая замена enum
const Status = {
  Active: "active",
  Inactive: "inactive",
  Pending: "pending",
} as const;

// Получаем union тип из объекта
type Status = typeof Status[keyof typeof Status];
// Тип: "active" | "inactive" | "pending"

// Используем как объект (runtime) и как тип (compile-time)
function getStatusLabel(status: Status): string {
  switch (status) {
    case Status.Active:   return "Активен";
    case Status.Inactive: return "Неактивен";
    case Status.Pending:  return "В ожидании";
  }
}

// TypeScript знает все значения!
const s: Status = Status.Active; // ✓ OK
// const bad: Status = "unknown"; // ✗ Ошибка!
8

Narrowing с литеральными типами (Сужение типов)

Narrowing (сужение типов) — это процесс, при котором TypeScript автоматически определяет более узкий тип на основе проверок в коде. Литеральные типы делают narrowing ещё более мощным!

type Shape = 
  | { kind: "circle"; radius: number }
  | { kind: "rectangle"; width: number; height: number }
  | { kind: "triangle"; base: number; height: number };

function getArea(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      // TypeScript знает, что здесь shape — { kind: "circle"; radius: number }
      return Math.PI * shape.radius ** 2;
    
    case "rectangle":
      // TypeScript знает, что здесь shape — { kind: "rectangle"; width: number; height: number }
      return shape.width * shape.height;
    
    case "triangle":
      // TypeScript знает, что здесь shape — { kind: "triangle"; base: number; height: number }
      return (shape.base * shape.height) / 2;
    
    default:
      // Exhaustive check — TypeScript проверяет, что все варианты обработаны
      const _exhaustive: never = shape;
      return _exhaustive;
  }
}

const circle: Shape = { kind: "circle", radius: 5 };
console.log(getArea(circle)); // 78.53981633974483

Narrowing с if и литеральными типами:

type Response = 
  | { status: "success"; data: unknown }
  | { status: "error"; error: string }
  | { status: "loading" };

function handleResponse(response: Response) {
  if (response.status === "success") {
    // TypeScript знает, что здесь response — { status: "success"; data: unknown }
    console.log("Данные:", response.data);
    return;
  }
  
  if (response.status === "error") {
    // TypeScript знает, что здесь response — { status: "error"; error: string }
    console.error("Ошибка:", response.error);
    return;
  }
  
  // Если дошли сюда, response.status === "loading"
  console.log("Загрузка...");
}

💡 Совет: Всегда проверяйте все варианты union-типа! TypeScript помогает с switch и if, но вы должны явно обработать каждый случай. Это предотвращает забытые ветки и делает код надёжнее.

Exhaustive checking — убедитесь, что все варианты обработаны:

type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";

function assertNever(value: never): never {
  throw new Error(`Unexpected value: ${value}`);
}

function handleMethod(method: HttpMethod) {
  switch (method) {
    case "GET":
      console.log("Получение данных");
      break;
    case "POST":
      console.log("Создание данных");
      break;
    case "PUT":
      console.log("Обновление данных");
      break;
    case "DELETE":
      console.log("Удаление данных");
      break;
    default:
      // Если добавить новый метод в HttpMethod и забыть обработать здесь —
      // TypeScript покажет ошибку компиляции!
      assertNever(method);
  }
}

Narrowing с typeof и литеральными типами:

type Primitive = string | number | boolean;

function formatValue(value: Primitive): string {
  if (typeof value === "string") {
    // TypeScript знает, что value — string
    return value.toUpperCase();
  }
  if (typeof value === "number") {
    // TypeScript знает, что value — number
    return value.toFixed(2);
  }
  // TypeScript знает, что value — boolean
  return value ? "Да" : "Нет";
}

console.log(formatValue("hello"));  // "HELLO"
console.log(formatValue(3.14159));  // "3.14"
console.log(formatValue(true));     // "Да"
9

Type Aliases с литеральными типами

Type aliases (type) позволяют давать имена литеральным типам, делая код более читаемым и самодокументирующимся.

// Создаём именованные типы для документирования намерений
type UserId = string & { __brand: "UserId" };
type OrderId = string & { __brand: "OrderId" };

// Функция принимает только UserId
function getUser(id: UserId) {
  console.log(`Получаем пользователя ${id}`);
}

// Нельзя передать OrderId в функцию для User!
// getUser(orderId as UserId); // ✗ Ошибка! (если brand не совпадает)

Практический пример — типизированные CSS-переменные:

type CSSColor = 
  | "red" | "blue" | "green" | "yellow" | "white" | "black"
  | "rgba(255,0,0,0.5)" | "rgba(0,0,255,0.5)"
  | `#${string}`;

type CSSSize = `${number}px` | `${number}em` | `${number}rem` | `${number}%`;

interface CSSProperties {
  color?: CSSColor;
  fontSize?: CSSSize;
  margin?: CSSSize;
  padding?: CSSSize;
  backgroundColor?: CSSColor;
}

const buttonStyle: CSSProperties = {
  color: "white",
  fontSize: "16px",
  backgroundColor: "blue",
  padding: "12px 24px",
};

// Ошибки — невалидные CSS-значения!
// const badStyle: CSSProperties = { color: "rgb(255,0,0)" }; // ✗ Не поддерживается
// const badSize: CSSProperties = { fontSize: "big" }; // ✗ Не число

Типизированныесобытие-хэндлеры:

type EventName = "click" | "hover" | "focus" | "blur" | "keydown" | "keyup";
type EventHandler = (event: Event) => void;

class EventEmitter {
  private handlers: Record<EventName, EventHandler[]> = {
    click: [],
    hover: [],
    focus: [],
    blur: [],
    keydown: [],
    keyup: [],
  };

  on(event: EventName, handler: EventHandler) {
    this.handlers[event].push(handler);
  }

  emit(event: EventName, eventData: Event) {
    this.handlers[event].forEach(handler => handler(eventData));
  }
}

const emitter = new EventEmitter();
emitter.on("click", (e) => console.log("Клик!", e));
// emitter.on("scroll", () => {}); // ✗ Ошибка! "scroll" не в списке
10

Discriminated Unions с литеральными типами

Discriminated Unions (дискриминированные объединения) — это один из самых мощных паттернов в TypeScript. Он комбинирует union-типы, литеральные типы и narrowing для создания безопасных и выразительных структур данных.

// Каждый вариант имеет свой литеральный "discriminant" (дискриминатор)
type Result<T> =
  | { status: "success"; data: T }
  | { status: "error"; error: string; code: number }
  | { status: "loading" };

function handleResult<T>(result: Result<T>) {
  // TypeScript автоматически определяет, какой это вариант
  switch (result.status) {
    case "success":
      // result.data доступен — TypeScript знает тип!
      console.log("Данные получены:", result.data);
      break;
    
    case "error":
      // result.error и result.code доступны!
      console.error(`Ошибка ${result.code}: ${result.error}`);
      break;
    
    case "loading":
      // Нет дополнительных полей
      console.log("Загрузка...");
      break;
  }
}

Пример с API-ответами:

interface User {
  id: number;
  name: string;
  email: string;
}

interface Post {
  id: number;
  title: string;
  content: string;
}

type ApiResponse<T> =
  | { type: "success"; data: T; timestamp: number }
  | { type: "error"; error: string; status: number }
  | { type: "network_error"; retryAfter: number }
  | { type: "timeout" };

async function fetchUser(id: number): Promise<ApiResponse<User>> {
  try {
    const response = await fetch(`/api/users/${id}`);
    if (!response.ok) {
      return { type: "error", error: response.statusText, status: response.status };
    }
    const data = await response.json();
    return { type: "success", data, timestamp: Date.now() };
  } catch (e) {
    if (e instanceof TypeError) {
      return { type: "network_error", retryAfter: 5000 };
    }
    return { type: "timeout" };
  }
}

async function displayUser(id: number) {
  const result = await fetchUser(id);
  
  switch (result.type) {
    case "success":
      console.log(`Пользователь: ${result.data.name}`); // ✓ data доступен
      break;
    case "error":
      console.error(`Ошибка ${result.status}: ${result.error}`); // ✓ error доступен
      break;
    case "network_error":
      console.log(`Повтор через ${result.retryAfter}мс`); // ✓ retryAfter доступен
      break;
    case "timeout":
      console.log("Превышено время ожидания");
      break;
  }
}

Discriminated unions с Exhaustive checks:

type Shape = 
  | { kind: "circle"; radius: number }
  | { kind: "rectangle"; width: number; height: number }
  | { kind: "triangle"; base: number; height: number };

function calculateArea(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "rectangle":
      return shape.width * shape.height;
    case "triangle":
      return 0.5 * shape.base * shape.height;
    default:
      // Если добавить новый kind в Shape и забыть обработать —
      // TypeScript покажет ошибку!
      const _exhaustive: never = shape;
      return _exhaustive;
  }
}

// Если добавить "pentagon" в Shape:
// type Shape = ... | { kind: "pentagon"; sides: number };
// TypeScript покажет ошибку в default-ветке!

💡 Совет: Discriminated unions — это золотой стандарт для обработки множества вариантов в TypeScript. Они заменяют switch-case по строкам, делая кодтипобезопасный и самодокументирующимся. Всегда используйте литеральный тип в качестве дискриминатора!

11

Практические паттерны с литеральными типами

Давайте рассмотрим реальные паттерны, которые вы будете использовать каждый день:

Паттерн 1: Type-safeсобытиеы

// Типизированная система событий
type EventMap = {
  "user:login": { userId: string; timestamp: number };
  "user:logout": { userId: string };
  "order:created": { orderId: string; amount: number };
  "order:shipped": { orderId: string; trackingNumber: string };
};

class TypedEventEmitter<T extends Record<string, unknown>> {
  private handlers: { [K in keyof T]?: ((data: T[K]) => void)[] } = {};

  on<K extends keyof T>(event: K, handler: (data: T[K]) => void) {
    if (!this.handlers[event]) {
      this.handlers[event] = [];
    }
    this.handlers[event]!.push(handler);
  }

  emit<K extends keyof T>(event: K, data: T[K]) {
    this.handlers[event]?.forEach(handler => handler(data));
  }
}

const events = new TypedEventEmitter<EventMap>();

// TypeScript проверяет тип данных!
events.on("user:login", (data) => {
  console.log(`Пользователь ${data.userId} вошёл в систему`);
  // data.timestamp доступен — TypeScript знает тип!
});

events.emit("user:login", { userId: "123", timestamp: Date.now() });
// events.emit("user:login", { userId: "123" }); // ✗ Ошибка! Нет timestamp

Паттерн 2: Builder с литеральными типами

// Типизированный builder для HTTP-запросов
interface RequestBuilder<Method extends string = never, URL extends string = never> {
  method<M extends string>(): RequestBuilder<M, URL>;
  url<U extends string>(): RequestBuilder<Method, U>;
  build(): { method: Method; url: URL };
}

class RequestBuilderImpl implements RequestBuilder {
  private _method = "";
  private _url = "";

  method<M extends string>(): RequestBuilder<M, string> {
    this._method = "" as M;
    return this as any;
  }

  url<U extends string>(): RequestBuilder<string, U> {
    this._url = "" as U;
    return this as any;
  }

  build() {
    return { method: this._method, url: this._url };
  }
}

// TypeScript проверяет порядок вызовов!
const request = new RequestBuilderImpl()
  .method<"GET">()
  .url<"/api/users">()
  .build();
// Тип: { method: "GET"; url: "/api/users" }

Паттерн 3: Type-safe настройки

// Конфигурация с автоматическим выводом типов
const defaultConfig = {
  theme: "light" as const,
  language: "ru" as const,
  notifications: true as const,
  maxRetries: 3 as const,
} satisfies Record<string, unknown>;

type Config = typeof defaultConfig;

// Функция для обновления конфигурации
function updateConfig<K extends keyof Config>(
  key: K,
  value: Config[K]
): Config {
  // ... обновление
  return { ...defaultConfig, [key]: value };
}

// TypeScript знает тип каждого ключа!
updateConfig("theme", "dark");    // ✓ OK (если "dark" в допустимых)
updateConfig("maxRetries", 5);    // ✓ OK
// updateConfig("theme", 123);    // ✗ Ошибка! theme — строка
// updateConfig("unknown", "x");  // ✗ Ошибка! "unknown" не в ключах
12

Типичные ошибки и как их избежать

Ошибка 1: Забытый кейс в switch

// ✗ Плохо — нет проверки всех вариантов
type Status = "active" | "inactive" | "pending";

function getStatusLabel(status: Status): string {
  switch (status) {
    case "active":   return "Активен";
    case "inactive": return "Неактивен";
    // Где pending?! TypeScript НЕ покажет ошибку!
  }
}

// ✓ Хорошо — exhaustive check
function getStatusLabel(status: Status): string {
  switch (status) {
    case "active":   return "Активен";
    case "inactive": return "Неактивен";
    case "pending":  return "В ожидании";
    default:
      const _exhaustive: never = status;
      return _exhaustive;
  }
}

Ошибка 2: Использование enum вместо литерального union

// ✗ Плохо — enum генерирует лишний JS-код
enum Direction {
  Up = "UP",
  Down = "DOWN",
  Left = "LEFT",
  Right = "RIGHT",
}

// ✓ Хорошо — литеральный union + const object
const Direction = {
  Up: "UP",
  Down: "DOWN",
  Left: "LEFT",
  Right: "RIGHT",
} as const;

type Direction = typeof Direction[keyof typeof Direction];

Ошибка 3: Потеря литерального типа при присваивании

// ✗ Плохо — переменная получает широкий тип
const status = "active";
// Тип: string (не "active"!)

// ✓ Хорошо — as const сохраняет литеральный тип
const status = "active" as const;
// Тип: "active"

// ✓ Или используйте явную аннотацию
const status: "active" = "active";

Ошибка 4: Смешивание enum и литеральных типов

// ✗ Плохо — enum и литеральный union в одном коде
enum Status1 { Active, Inactive }
type Status2 = "active" | "inactive";

// Функция не может принимать оба типа!
function process(status: Status1 | Status2) {
  // Придётся проверять typeof — неудобно
}

// ✓ Хорошо — выберите ОДИН подход и используйте его везде
type Status = "active" | "inactive";

⚠️ Предупреждение: Не создавайте литеральные типы с сотнями значений! Если у вас более 20-30 литералов, подумайте о string с валидацией в рантайме. Литеральные типы — для фиксированного набора значений, а не для пользовательского ввода.

✓

Итоги урока

Отлично! Мы подробно изучили литеральные типы — один из самых мощных инструментов TypeScript. Давайте подведём итоги:

  • Литеральный тип — это тип, соответствующий конкретному значению (строке, числу, boolean)
  • Union литеральных типов ограничивает значение фиксированным набором допустимых вариантов
  • Шаблонные литеральные типы позволяют создавать строки по шаблону на уровне типов
  • as const сохраняет литеральные типы и делает значения readonly
  • Discriminated unions комбинируют литеральные типы с union для безопасной обработки множества вариантов
  • Narrowing позволяет TypeScript автоматически определять точный тип на основе проверок

🎯 Что дальше: На следующем уроке мы изучим условные типы (Conditional Types) — ещё более продвинутый инструмент, который позволяет создавать типы на основе условий. Литеральные типы — основа для понимания условных типов, поэтому вы уже сделали большой шаг!

🎯

Практическое задание

Попрактикуйтесь! Создайте:

  1. Тип WeekDay — union из литералов "Monday"..."Sunday"
  2. Discriminated union Action с вариантами "increment", "decrement", "reset"
  3. Шаблонный литеральный тип EventHandler для имён событий "onClick", "onSubmit" и т.д.
  4. Функцию calculate с discriminated union для разных типов вычислений
← Предыдущий урок
Типы-пересечения (7.2)
Следующий урок →
Условные типы (7.4)

Тест по Литеральным типам

10 вопросов

Статусы заказа на литеральных типах

Premium

Template literal types для событий

Premium