Литеральные типы

Литеральные типы — конкретные значения как типы: не просто string, а именно "hello"; не number, а именно 42.

Зачем нужно

  • Ограничение допустимых значений до конкретного набора
  • Более точная типизация: "GET" | "POST" вместо string
  • Основа для discriminated unions и pattern matching
  • Template literal types — мощные строковые паттерны на уровне типов

Где используется

  • HTTP-методы: "GET" | "POST" | "PUT" | "DELETE"
  • Событийные системы: конкретные имена событий
  • Конфигурации: ограниченный набор опций
  • API: точное описание допустимых параметров

Предпосылки

String Literal Types

// Тип — конкретная строка
type Direction = "up" | "down" | "left" | "right";

let dir: Direction = "up";    // OK
dir = "down";                  // OK
dir = "diagonal";              // Ошибка! Type '"diagonal"' is not assignable

// Параметры функций
function move(direction: "up" | "down" | "left" | "right"): void {
  console.log(`Moving ${direction}`);
}

move("up");    // OK
move("UP");    // Ошибка! Регистр имеет значение

// HTTP-методы
type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";

function fetch(url: string, method: HttpMethod): void {
  // ...
}

Number Literal Types

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

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

// Битовые значения
type BitFlag = 0 | 1;

// Порты
type CommonPort = 80 | 443 | 3000 | 8080;

Boolean Literal Types

// true или false как тип
type True = true;
type False = false;

// Полезно в generics
type IsString<T> = T extends string ? true : false;

type A = IsString<"hello">; // true
type B = IsString<42>;       // false

// Объект с флагом
interface Config {
  debug: true; // Только true!
}

as const

as const сужает тип до максимально конкретного литерального типа:

// Без as const
let method = "GET";        // string
const method2 = "GET";     // "GET" (const сужает)

// as const для объектов
const config = {
  url: "https://api.example.com",
  method: "GET",
  timeout: 5000,
};
// Тип: { url: string; method: string; timeout: number }

const config = {
  url: "https://api.example.com",
  method: "GET",
  timeout: 5000,
} as const;
// Тип: {
//   readonly url: "https://api.example.com";
//   readonly method: "GET";
//   readonly timeout: 5000;
// }

// as const для массивов
const methods = ["GET", "POST", "PUT"] as const;
// Тип: readonly ["GET", "POST", "PUT"]

// Извлечение union из as const массива
type Method = (typeof methods)[number]; // "GET" | "POST" | "PUT"

as const и функции

// Проблема: литеральный тип теряется при передаче в функцию
function request(method: "GET" | "POST", url: string): void {}

const options = { method: "GET", url: "/api" };
request(options.method, options.url);
// Ошибка! options.method — string, а не "GET"

// Решение 1: as const на объекте
const options = { method: "GET", url: "/api" } as const;
request(options.method, options.url); // OK

// Решение 2: as const на значении
const options = { method: "GET" as const, url: "/api" };
request(options.method, options.url); // OK

// Решение 3: satisfies (TypeScript 5.0+)
const options = {
  method: "GET",
  url: "/api",
} as const satisfies { method: "GET" | "POST"; url: string };

Template Literal Types

Строковые шаблоны на уровне типов (TypeScript 4.1+):

// Базовый синтаксис
type Greeting = `Hello, ${string}`;

let g: Greeting = "Hello, World"; // OK
let g2: Greeting = "Hi, World";   // Ошибка!

// Комбинация литералов
type Size = "small" | "medium" | "large";
type Color = "red" | "blue" | "green";

type ColoredSize = `${Color}-${Size}`;
// "red-small" | "red-medium" | "red-large" |
// "blue-small" | "blue-medium" | "blue-large" |
// "green-small" | "green-medium" | "green-large"

// Event handlers
type EventName = "click" | "focus" | "blur";
type Handler = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur"

CSS-подобные типы

type CSSUnit = "px" | "em" | "rem" | "%";
type CSSValue = `${number}${CSSUnit}`;

let width: CSSValue = "100px";   // OK
let height: CSSValue = "50%";    // OK
let bad: CSSValue = "100";       // Ошибка!
let bad2: CSSValue = "wide";     // Ошибка!

Utility Types для строк

TypeScript имеет встроенные типы для преобразования строк:

type Upper = Uppercase<"hello">;       // "HELLO"
type Lower = Lowercase<"HELLO">;       // "hello"
type Cap = Capitalize<"hello">;        // "Hello"
type Uncap = Uncapitalize<"Hello">;    // "hello"

// Комбинирование
type Event = "click" | "scroll" | "resize";
type OnEvent = `on${Capitalize<Event>}`;
// "onClick" | "onScroll" | "onResize"

// Getter/Setter типы
type Prop = "name" | "age" | "email";
type Getter = `get${Capitalize<Prop>}`;
// "getName" | "getAge" | "getEmail"
type Setter = `set${Capitalize<Prop>}`;
// "setName" | "setAge" | "setEmail"

Template Literal Types с infer

// Извлечение частей строки
type ExtractId<S extends string> = S extends `user_${infer Id}` ? Id : never;

type UserId = ExtractId<"user_123">; // "123"
type Invalid = ExtractId<"admin_456">; // never

// Парсинг путей
type ExtractParams<Path extends string> =
  Path extends `${string}:${infer Param}/${infer Rest}`
    ? Param | ExtractParams<Rest>
    : Path extends `${string}:${infer Param}`
    ? Param
    : never;

type Params = ExtractParams<"/users/:userId/posts/:postId">;
// "userId" | "postId"

Паттерн: Создание типов из данных

// Массив допустимых значений + тип из него
const ROLES = ["admin", "user", "guest"] as const;
type Role = (typeof ROLES)[number]; // "admin" | "user" | "guest"

// Проверка в рантайме
function isRole(value: string): value is Role {
  return (ROLES as readonly string).includes(value);
}

// Объект + тип из ключей/значений
const STATUS_MAP = {
  active: "ACTIVE",
  inactive: "INACTIVE",
  banned: "BANNED",
} as const;

type StatusKey = keyof typeof STATUS_MAP;
// "active" | "inactive" | "banned"

type StatusValue = (typeof STATUS_MAP)[StatusKey];
// "ACTIVE" | "INACTIVE" | "BANNED"

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

  1. let vs constlet расширяет тип, const сужает до литерала
const x = "hello"; // тип: "hello"
let y = "hello";   // тип: string

// Для let нужна явная аннотация
let z: "hello" | "world" = "hello";
  1. Потеря литерального типа при передаче — объект без as const расширяет типы
  2. Слишком большой union из template literals — может замедлить компиляцию
// Осторожно: 10 * 10 * 10 = 1000 вариантов
type Big = `${0|1|2|3|4|5|6|7|8|9}${0|1|2|3|4|5|6|7|8|9}${0|1|2|3|4|5|6|7|8|9}`;
  1. Забыть as const при извлечении union из массива — без него будет просто string

Практика

  1. Создайте тип HttpMethod из литералов и функцию принимающую только эти значения
  2. Используйте as const для объекта конфигурации и извлеките типы
  3. Создайте template literal type для CSS-значений: "${number}px" или "${number}%"
  4. Напишите тип EventHandler<E> который превращает "click" в "onClick"
  5. Создайте массив as const и извлеките union type из его элементов

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

Ресурсы