Мы уже прошли значительный путь, изучив базовые типы, интерфейсы и множество других концепций. Сегодня нас ждет очень важная и элегантная тема, это Литеральные типы (Literal Types). Один из тех инструментов, который кажется простым на первый взгляд, но открывает потрясающие возможности для написания строго типизированного и надёжного кода.
Что такое литеральные типы?
Давай начнём с самого фундаментального вопроса: что же это такое? В TypeScript литеральный тип это тип, который представляет собой не просто строку, число или булево значение, а одно конкретное, фиксированное значение.
Звучит немного запутанно? Давай разбираться на примерах. До сих пор мы говорили: «Эта переменная должна быть строкой».
let message: string; message = "Привет"; // OK message = "Пока"; // OK message = "123"; // OK. Всё, что строка, подходит.
С литерными типами мы говорим не «это строка», а «это конкретная строка». Например: «Эта переменная может быть только строкой ‘success’ или только строкой ‘error'».
let status: 'success'; status = 'success'; // OK status = 'error'; // Ошибка: Type '"error"' is not assignable to type '"success"'.
Видишь магию? Мы создали тип, который принимает только одно-единственное значение строку 'success'. Это и есть простейший литеральный тип. Но его сила раскрывается в полной мере, когда мы комбинируем его с оператором объединения (|), который мы уже знаем и любим.
// Теперь status может быть одним из двух КОНКРЕТНЫХ значений let status: 'success' | 'error'; status = 'success'; // OK status = 'error'; // OK status = 'pending'; // Ошибка: Type '"pending"' is not assignable to type '"success" | "error"'.
Вот теперь это по-настояшно мощно! Мы объявили тип, который может быть либо литералом 'success', либо литералом 'error'. Любая другая строка, даже очень похожая, вызовет ошибку на этапе компиляции. Это не просто string, это строго ограниченный набор возможных строк. TypeScript не позволит нам ошибиться и присвоить недопустимое значение.
Зачем нужны литеральные типы?
Теперь резонный вопрос: зачем это нужно? Не проще ли использовать обычные string или number? Проще, но гораздо менее надёжно. Сила литеральных типов заключается в создании сверхточных контрактов для нашего кода.
-
Повышение читабельности и надёжности: Код становится самодокументируемым. Глядя на объявление
function setAlignment(align: 'left' | 'center' | 'right') {...}, сразу понятно, какие значения функция ожидает. Не нужно гадать, принимает ли она'centre'(британский вариант) или только'center'. -
Исключение ошибок на этапе компиляции: Это главное преимущество TypeScript! Если мы попытаемся передать в функцию
setAlignmentзначение'top', компилятор тут же укажет нам на ошибку. Мы поймаем баг ещё до запуска кода, в IDE, а не в процессе выполнения программы, где его уже мог увидеть пользователь. -
Идеальная совместимость с существующим JavaScript-кодом: Многие JavaScript-библиотеки и API уже используют на практике подобный подход, например аргумент может принимать строки
'yes'/'no'или'on'/'off'. Литеральные типы позволяют описать этот шаблон на уровне типов без необходимости переписывать сам код. -
Основа для более сложных паттернов: Как мы увидим позже в курсе, литеральные типы являются кирпичиками для таких продвинутых концепций, как тегированные (размеченные) объединения (discriminated unions), которые radically меняют подход к структурированию данных.
По сути, использование литеральных типов, это переход от мысли «это строка» к мысли «это конкретное значение из известного набора вариантов». Это шаг в сторону более точного и выразительного моделирования предметной области.
Синтаксис литеральных типов
Синтаксис до безобразия прост и интуитивно понятен. Ты буквально используешь то значение, которое переменная должна принимать.
Строковые литеральные типы (String Literal Types):
Используются чаще всего.
let direction: 'up' | 'down' | 'left' | 'right'; direction = 'up'; // OK direction = 'north'; // Ошибка let eventType: 'click' | 'mouseover' | 'keydown'; eventType = 'click'; // OK eventType = 'scroll'; // Ошибка
Числовые литеральные типы (Numeric Literal Types):
Работают абсолютно аналогично строковым. Не так популярны, но бывают чрезвычайно полезны, например, для статусных кодов HTTP или кодов ошибок.
let httpStatus: 200 | 404 | 500; httpStatus = 200; // OK httpStatus = 403; // Ошибка: Type '403' is not assignable to type '200 | 404 | 500'. // Часто используется для точного описания возможных состояний let diceRoll: 1 | 2 | 3 | 4 | 5 | 6; diceRoll = 3; // OK diceRoll = 7; // Ошибка: У нас просто нет кубика на 7 граней!
Булевы литеральные типы (Boolean Literal Types):
Да, technically, тип true или false это тоже литеральный тип. Но их использование в одиночку (let isDone: true;) это скорее курьёз, так как такая переменная никогда не сможет стать false. Однако в составе объединений они могут быть полезны.
// Само по себе ограничение только на true не очень полезно let isAbsolutelyTrue: true; isAbsolutelyTrue = true; // OK isAbsolutelyTrue = false; // Ошибка // Но может быть частью более сложной логики let isEnabled: true | null; // Может быть истиной или отсутствовать
Сужение типов с помощью литералов
Одна из самых крутых особенностей работы с литеральными типами в TypeScript это то, как система типов использует их для сужения типа (type narrowing) внутри блоков кода, таких как if, switch или проверки на существование.
Рассмотрим на практическом примере. Допустим, у нас есть функция, которая обрабатывает ответ от сервера.
function handleResponse(response: { status: 'success' | 'error', data?: string, message?: string }) { // В этом месте TypeScript знает, что response.status это 'success' | 'error' console.log(`Статус: ${response.status}`); if (response.status === 'success') { // Здесь TypeScript СУЗИЛ тип response.status до литерала 'success'! // А раз статус 'success', то согласно нашему интерфейсу, поле data ОБЯЗАТЕЛЬНО существует. console.log(`Данные: ${response.data.toUpperCase()}`); // Безопасно! TypeScript это знает. // console.log(response.message); // Ошибка! TypeScript знает, что message здесь может не быть. } else { // А здесь TypeScript понимает, что статус может быть только 'error' (так как 'success' мы уже проверили). // Следовательно, существует поле message. console.log(`Ошибка: ${response.message}`); // Безопасно! // console.log(response.data); // Ошибка! data для статуса 'error' может не быть. } } // Примеры вызова handleResponse({ status: 'success', data: 'Всё окей!' }); handleResponse({ status: 'error', message: 'Всё сломалось!' });
Это мощнейший механизм! TypeScript анализирует наши проверки и внутри блоков кода понимает, какой именно из вариантов объединения сейчас в работе. Это позволяет обращаться к свойствам, которые специфичны для данного литерального значения, полностью обеспечивая типобезопасность.
Создание псевдонимов для литеральных типов
Вспомним наш друг type alias. Мы можем и должны создавать псевдонимы для сложных объединений литеральных типов. Это делает код чище и переиспользуемым.
// Вместо того чтобы везде писать длинные объединения... function setStatus(s: 'pending' | 'in-progress' | 'completed'): void { ... } function getStatus(): 'pending' | 'in-progress' | 'completed' { ... } // ...мы можем вынести это в отдельный псевдоним типа type Status = 'pending' | 'in-progress' | 'completed'; function setStatus(s: Status): void { // логика } function getStatus(): Status { // логика return 'completed'; } // Это также отлично работает и с числовыми литералами type HttpCode = 200 | 201 | 400 | 401 | 403 | 404 | 500; let myStatus: HttpCode = 404;
Литеральные типы и проверка на этапе компиляции
Важно понимать, что это все происходит только на этапе компиляции. В скомпилированном JavaScript-коде никаких упоминаний о типах не останется. Следующий код:
let status: 'success' | 'error'; status = 'success';
Будет скомпилирован в простой JS:
let status; status = 'success';
Это означает, что во время выполнения никто не помешает переменной status принять любое строковое значение, если оно будет присвоено каким-то внешним кодом, не проверяемым TypeScript. Сила литеральных типов в предотвращении ошибок на этапе написания кода, а не его выполнения.
Литеральные типы в сравнении с enum
У тебя мог возникнуть вопрос: «Максим, но ведь для похожих задач мы уже использовали enum! В чём разница?» Отличный вопрос!
Enum (перечисления) создают новый тип и новое пространство имён в рантайме (во время выполнения). Они компилируются в реальный JavaScript-объект.
Литеральные типы это конструкция, существующая только на этапе компиляции. Они не генерируют лишнего кода.
Пример с enum:
enum StatusEnum { Success = 'success', Error = 'error' } let enumStatus: StatusEnum = StatusEnum.Success; // Компилируется в: // var StatusEnum; // (function (StatusEnum) { // StatusEnum["Success"] = "success"; // StatusEnum["Error"] = "error"; // })(StatusEnum || (StatusEnum = {})); // let enumStatus = StatusEnum.Success;
Пример с литеральным типом:
type StatusLiteral = 'success' | 'error'; let literalStatus: StatusLiteral = 'success'; // Компилируется в: // let literalStatus = 'success';
Когда что использовать?
-
Используйте
enum, когда вам важно иметь объект, существующий во время выполнения. Например, для итерации по всем значениям (Object.values(StatusEnum)) или для использования в runtime-проверках. -
Используйте литеральные типы, когда вам нужно просто ограничить множество строковых/числовых значений и не нужна дополнительная функциональность во время выполнения. Это часто приводит к более простому и лаконичному коду.
В современном TypeScript среди разработчиков есть тенденция предпочитать литеральные типы из-за их простоты и отсутствия накладных расходов, если только не нужны специфические возможности enum.
Практические примеры и задачи
Давай закрепим материал на реальных кейсах.
Задача 1: Функция для работы с API
Напиши функцию fetchData, которая принимает два аргумента: url (строка) и method (литеральный тип, который может принимать значения 'GET', 'POST', 'PUT', 'DELETE'). Функция должна возвращать промис с любыми данными (пока просто используй any для ответа). Продемонстрируй вызов функции с правильным и неправильным методом.
Решение:
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'; async function fetchData(url: string, method: HttpMethod): Promise<any> { // Здесь была бы реальная логика fetch console.log(`Выполняю ${method} запрос на ${url}`); return Promise.resolve({data: 'test'}); // Заглушка } // Использование: fetchData('https://api.example.com/users', 'GET').then(console.log); // OK fetchData('https://api.example.com/users', 'POST').then(console.log); // OK fetchData('https://api.example.com/users', 'PATCH').then(console.log); // Ошибка: Argument of type '"PATCH"' is not assignable to parameter of type 'HttpMethod'.
Задача 2: Обработчик событий
Создай тип EventType для событий 'click', 'doubleClick', 'keyPress'. Напиши функцию-обработчик handleEvent, которая принимает событие этого типа и в зависимости от него выводит в консоль разное сообщение. Используй сужение типов.
Решение:
type EventType = 'click' | 'doubleClick' | 'keyPress'; function handleEvent(event: EventType): void { if (event === 'click') { console.log('Произведён клик!'); } else if (event === 'doubleClick') { console.log('Двойной клик!'); } else { // TypeScript здесь знает, что event может быть только 'keyPress' console.log(`Нажата клавиша: ...`); // Здесь могла бы быть более сложная логика } } // Альтернативное решение с switch (идеально подходит для литеральных типов) function handleEventSwitch(event: EventType): void { switch (event) { case 'click': console.log('Произведён клик!'); break; case 'doubleClick': console.log('Двойной клик!'); break; case 'keyPress': console.log('Нажата клавиша!'); break; } } handleEvent('click'); // OK handleEvent('scroll'); // Ошибка
Задача 3: Система ролей пользователя
Опиши тип UserRole, который может быть 'admin', 'editor', 'viewer'. Создай функцию getDashboardLink, которая возвращает разные URL в зависимости от роли. Для роли 'admin' должен возвращаться '/admin', для 'editor' — '/editor', для 'viewer' — '/dashboard'.
Решение:
type UserRole = 'admin' | 'editor' | 'viewer'; function getDashboardLink(role: UserRole): string { // Используем сужение типов для возврата нужного значения if (role === 'admin') { return '/admin'; } else if (role === 'editor') { return '/editor'; } else { return '/dashboard'; } } // Или, что более идиоматично, с помощью switch function getDashboardLinkSwitch(role: UserRole): string { switch (role) { case 'admin': return '/admin'; case 'editor': return '/editor'; case 'viewer': return '/dashboard'; } } console.log(getDashboardLink('admin')); // "/admin" console.log(getDashboardLink('user')); // Ошибка: Argument of type '"user"' is not assignable to parameter of type 'UserRole'.
Задача 4 (Продвинутая): Комбинирование с интерфейсами
Создай интерфейс ButtonProps для компонента кнопки. У него должно быть свойство type типа 'button' | 'submit' | 'reset' (это стандартные значения для HTML-кнопки) и свойство appearance типа 'primary' | 'secondary' | 'danger'. Создай объект, соответствующий этому интерфейсу.
Решение:
interface ButtonProps { type: 'button' | 'submit' | 'reset'; appearance: 'primary' | 'secondary' | 'danger'; text: string; // ... другие свойства } const myButton: ButtonProps = { type: 'button', appearance: 'primary', text: 'Нажми меня' }; const badButton: ButtonProps = { type: 'magic', // Ошибка: Type '"magic"' is not assignable to type '"button" | "submit" | "reset"'. appearance: 'primary', text: 'Не нажимай меня' };
Эта задача отлично демонстрирует, как литеральные типы интегрируются в более сложные структуры, делая их невероятно выразительными и безопасными.
Этот урок важный кирпичик в фундаменте твоих знаний. Впереди нас ждут ещё более интересные темы, такие как дженерики (обобщения) и те самые тегированные объединения, где литеральные типы играют ключевую роль.
Если ты хочешь упорядочить все эти знания и пройти весь путь от начинающего до уверенного TypeScript-разработчика, то жду тебя на полном курсе по ссылке ниже.
Поддержка автора осуществляется с помощью специальной формы ниже, предоставленной сервисом «ЮMoney». Все платёжные операции выполняются на защищённой странице сервиса, что обеспечивает их корректность и полную безопасность.


