Skip to main content

Літеральні типи в TypeScript

Literal types (літеральні типи) у TypeScript фіксують змінну на точних значеннях - як "up" або 404 - а не на широких категоріях типу string або number.

Теорія

TL;DR

  • Literal type - як іменне паркомісце: підходить тільки "up", не будь-який string
  • Головна різниця: string прийме "banana", а "up" | "down" - ні, компілятор зупинить на етапі перевірки
  • Використовуй, коли варіантів 3-10 і всі відомі на етапі розробки
  • TypeScript розширює літерали в деяких контекстах (масиви, прості об'єкти), якщо не використовувати as const

Швидкий приклад

typescript
// Без literal - приймає будь-який рядок type Status = string; function setStatus(s: Status) {} setStatus("banana"); // Помилки немає // З literal - тільки точні значення type Status = "loading" | "success" | "error"; function setStatus(s: Status) {} setStatus("banana"); // Помилка: '"banana"' is not assignable to type 'Status' setStatus("success"); // OK

Компілятор перевіряє точний токен, а не просто категорію типу. Неправильні значення блокуються до запуску.

Ключова різниця

Тип string приймає будь-яку послідовність символів. "loading" | "success" | "error" приймає тільки ці три токени. Ти обмінюєш гнучкість на гарантію. TypeScript перевіряє точне значення під час аналізу типів і видаляє все при компіляції, тому JavaScript бачить звичайні рядки. Накладних витрат у runtime немає.

Одна річ, яка дивує більшість розробників вперше: TypeScript автоматично розширює літерали в деяких контекстах. Напиши { dir: "up" } - і TypeScript виведе { dir: string }, а не { dir: "up" }. Літерал губиться. Щоб зберегти його, використовуй as const.

Коли використовувати

  • Стани UI: type Status = "draft" | "published" | "archived"
  • HTTP-коди: type Code = 200 | 201 | 404
  • Ключі вирівнювання і конфігурації: type Align = "left" | "center" | "right"
  • Не варто для призначеного для користувача вводу (email, ім'я) - використовуй string
  • Не варто для булевих флагів - використовуй boolean, хіба що потрібен тільки true

as const: фіксування літералів

typescript
// Без as const - TypeScript розширює до string[] const directions = ["up", "down"]; type Dir = typeof directions[0]; // string, не "up" // З as const - залишається readonly ["up", "down"] const directions = ["up", "down"] as const; type Dir = typeof directions[number]; // "up" | "down"

Це важливо, коли ти виводиш типи з масивів або об'єктів конфігурації в runtime.

Як це обробляє компілятор

TypeScript трактує literal types як брендовані примітиви під час перевірки типів. Він зіставляє точний токен "up" з членами union за структурною еквівалентністю. На виході (emit) літерали стають звичайними JS-значеннями - Node.js і браузер бачать тільки сирі рядки або числа. Без накладних витрат. Розширення (widening) відбувається в коваріантних контекстах (масиви, типи, що повертаються), якщо as const не фіксує виведення.

Типові помилки

Забули as const на масиві

typescript
const roles = ["admin", "user"]; // виводить string[] type Role = typeof roles[number]; // string, не "admin" | "user" // Виправлення: const roles = ["admin", "user"] as const; type Role = typeof roles[number]; // "admin" | "user"

Припущення, що literal types перевіряють значення в runtime

typescript
function validate(code: 200 | 404) { return true; } // TypeScript видаляє типи при компіляції. // JS бачить: function validate(code) { return true; } // Виправлення: використовуй Zod: z.union([z.literal(200), z.literal(404)])

Розширення типу, що повертається

typescript
// Повертає string, а не "red" | "green" function getColor(type: "error" | "success"): string { return type === "error" ? "red" : "green"; } // Виправлення: зафіксуй тип повернення function getColor(type: "error" | "success"): "red" | "green" { return type === "error" ? "red" : "green"; }

Якщо значення приходить з користувацького вводу або бази даних, в runtime це string незалежно від literal type. Використовуй type guard або Zod для перевірки.

Де зустрічається в реальному коді

  • React / shadcn-ui: variant: "primary" | "outline" в пропсах кнопки
  • tRPC: type Method = "GET" | "POST" у визначеннях маршрутів
  • TanStack Query: "pending" | "success" | "error" для статусів запитів
  • Zod: z.literal("published") у схемах для валідації API

Питання на співбесіді

Q: Яка різниця між type Foo = "a" і const foo = "a" as const?
A: type визначає псевдонім типу, який існує тільки на рівні типів і зникає при компіляції. as const створює const-змінну, чий виведений тип - літерал "a". Обидва дають тип "a", але as const також дає значення, доступне в runtime.

Q: Чи можна використовувати literal types з generics?
A: Так. function identity<T extends "foo" | "bar">(x: T): T обмежує generic літеральним union. Викликач отримує назад точний літерал, а не розширений тип.

Q: Коли краще обрати enum замість literal union?
A: Literal union компілюється в ніщо і простіший у написанні. Enum генерує об'єкт в runtime із зворотним маппінгом (число до імені). В більшості сучасних TypeScript-проектів literal union достатньо.

Q: Як discriminated unions (дискриміновані об'єднання) використовують literal types?
A: Літеральне поле слугує дискримінантом. TypeScript автоматично звужує тип у кожній гілці.

typescript
type ApiResponse<T> = | { status: "success"; data: T } | { status: "error"; error: string }; // В обробнику: if (response.status === "success") { console.log(response.data); // TypeScript знає, що data тут є }

Приклади

Базовий: обробник напрямків

typescript
type Direction = "up" | "down" | "left" | "right"; function move(dir: Direction, distance: number) { console.log(`Move ${distance}px ${dir}`); } move("up", 10); // OK - виводить "Move 10px up" move("left", 5); // OK move("diagonal", 3); // Помилка: '"diagonal"' is not assignable to type 'Direction'

TypeScript відловлює неправильне значення до того, як код потрапить на продакшен.

Проміжний: компонент кнопки

typescript
type ButtonVariant = "primary" | "secondary" | "danger"; interface ButtonProps { variant: ButtonVariant; label: string; } function Button({ variant, label }: ButtonProps) { return `<button class="btn btn-${variant}">${label}</button>`; } Button({ variant: "primary", label: "Зберегти" }); // OK - рендерить btn-primary Button({ variant: "warning", label: "Зберегти" }); // Помилка: '"warning"' is not assignable

Компонент приймає тільки ті варіанти, для яких є CSS-класи. Жодних сюрпризів у runtime.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?