Літеральні типи в TypeScript
Literal types (літеральні типи) у TypeScript фіксують змінну на точних значеннях - як "up" або 404 - а не на широких категоріях типу string або number.
Теорія
TL;DR
- Literal type - як іменне паркомісце: підходить тільки
"up", не будь-якийstring - Головна різниця:
stringприйме"banana", а"up" | "down"- ні, компілятор зупинить на етапі перевірки - Використовуй, коли варіантів 3-10 і всі відомі на етапі розробки
- TypeScript розширює літерали в деяких контекстах (масиви, прості об'єкти), якщо не використовувати
as const
Швидкий приклад
// Без 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: фіксування літералів
// Без 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 на масиві
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
function validate(code: 200 | 404) { return true; }
// TypeScript видаляє типи при компіляції.
// JS бачить: function validate(code) { return true; }
// Виправлення: використовуй Zod: z.union([z.literal(200), z.literal(404)])Розширення типу, що повертається
// Повертає 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 автоматично звужує тип у кожній гілці.
type ApiResponse<T> =
| { status: "success"; data: T }
| { status: "error"; error: string };
// В обробнику:
if (response.status === "success") {
console.log(response.data); // TypeScript знає, що data тут є
}Приклади
Базовий: обробник напрямків
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 відловлює неправильне значення до того, як код потрапить на продакшен.
Проміжний: компонент кнопки
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.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.