Литеральные типы: одиночные значения как типы и as const, который их хранит
Литеральный тип — это одно значение как тип: "GET", 200, true. Объединения литералов — лёгкие enum. const хранит литералы, let их расширяет, а as const фиксирует глубокие readonly-литералы на объектах, массивах и кортежах — приём, питающий конфиг-как-типы.
Вы определяете объект конфигурации метода { method: "GET" } и передаёте в fetch. Компилятор отклоняет: Type 'string' is not assignable to type '"GET" | "POST" | ...'. Но вы же написали "GET" — как оно стало string? Это расширение, и однострочное лекарство (as const) — тот же приём, что позволяет целому файлу конфигурации быть одновременно рантайм-данными и точным типом. Литеральные типы — то, как TypeScript превращает конкретные значения в гарантии времени компиляции.
Одно значение, используемое как тип
Каждое примитивное значение может быть и типом: "GET" — это тип, чей единственный обитатель — строка "GET". Так же 200, true, 42n. Сами по себе они редко полезны — но как объединения становятся точными, самодокументирующимися доменами:
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type StatusCode = 200 | 201 | 400 | 404 | 500;
type Toggle = true | false; // т.е. boolean
function request(method: HttpMethod, url: string) { /* ... */ }
request("GET", "/users"); // ✅
request("PATCH", "/users"); // Error: Argument of type '"PATCH"' is not
// // assignable to parameter of type 'HttpMethod'.Объединение литералов — замкнутое множество, которое проверщик принуждает, а редактор автодополняет. Это самый частый «enum» в современном TypeScript — и у него нулевой рантайм-след.
Расширение, снова: откуда берутся литералы и где теряются
Вы встретили расширение в уроке о выводе. Литералы — ровно то, что расширение выбрасывает. Решает ключевое слово связывания:
let m = "GET";
// ^? let m: string — `let` расширяет; литерал утрачен
const m2 = "GET";
// ^? const m2: "GET" — `const` хранит литералТак что const уже даёт литерал для примитивов. Ловушка — объекты, куда const не дотягивается до свойств:
const config = { method: "GET" };
// ^? const config: { method: string } — свойство расширено до string
request(config.method, "/x");
// Error: Argument of type 'string' is not assignable to parameter of type 'HttpMethod'.Вот в точности баг из Hook: config.method расширился до string, потому что свойство изменяемо, поэтому больше не входит в объединение HttpMethod. Такова цена встречи структурной типизации с расширением.
as const: глубоко, readonly, литерально
Лекарство — утверждение const. as const делает над своим операндом сразу три вещи: делает каждое свойство readonly, не даёт расширения (хранит литеральные типы) и превращает литералы массивов в кортежи вместо массивов.
const config = { method: "GET", retries: 3 } as const;
// ^? const config: { readonly method: "GET"; readonly retries: 3 }
request(config.method, "/x"); // ✅ теперь config.method это литерал "GET"Смотрите, как оно работает сверху донизу на вложенных данных, массивах и кортежах:
const before = ["a", "b"];
// ^? const before: string[]
const after = ["a", "b"] as const;
// ^? const after: readonly ["a", "b"] — readonly-кортеж литералов
const route = { path: "/users", methods: ["GET", "POST"] } as const;
// ^? const route: {
// readonly path: "/users";
// readonly methods: readonly ["GET", "POST"];
// }Без as const methods был бы string[]; с ним — точный кортеж readonly ["GET", "POST"]. Это движок «конфиг-как-типы»: напишите обычный объектный литерал, добавьте as const и выводите типы прямо из рантайм-значения (typeof config, config["methods"][number]) — единый источник истины, без дублирования.
▸Почему это работает
as const — не рантайм-приведение: типы TypeScript стираются, поэтому испускаемый JavaScript идентичен с ним и без него. Оно лишь меняет, как компилятор читает литерал: не расширять, пометить readonly, делать кортежи. Поэтому его безопасно сыпать на объекты конфигурации: нулевая рантайм-цена, чистая информация о типах.
Когда расширение кусает — и лекарство
Классический провал: объект конфигурации, чьи литеральные ключи тихо расширяются, а потом отвергаются функцией, ожидающей объединение литералов.
type Level = "debug" | "info" | "warn" | "error";
function setLevel(l: Level) { /* ... */ }
const settings = { level: "warn" };
setLevel(settings.level);
// Error: Argument of type 'string' is not assignable to parameter of type 'Level'.
// Лекарства (выберите одно):
const settings2 = { level: "warn" as const }; // сузить одно свойство
const settings3 = { level: "warn" } as const; // сузить весь объект
const settings4 = { level: "warn" as Level }; // аннотировать к целевому объединениюЛюбое из них сохраняет level достаточно точным, чтобы удовлетворить Level. Общий урок: когда строите данные, предназначенные для слота с объединением литералов, надо остановить расширение — либо as const, либо типизацией переменной прямо к объединению.
Объединения литералов против enum
Прежде чем тянуться к enum, спросите себя: нужно ли это значение как рантайм-объект, или вы просто хотите, чтобы проверщик отвергал всё вне известного множества? Чаще всего — второе, и ровно это делают объединения литералов при нулевой рантайм-цене. Вот как они сравниваются:
| Аспект | Объединение литералов | enum |
|---|---|---|
| Рантайм-эмиссия | Нет — чистый тип, стирается | Испускает JS-объект (числовой enum двунаправлен) |
| Совместимость | Структурная — подходящая строка просто работает | Почти номинальная — надо ссылаться на член enum |
| Значения | Сам литерал и есть значение (“GET”) | Член отображается в отдельное значение |
| Дружелюбие к JSON / API | Высокое — значение на проводе и есть тип | Ниже — нужно отображать в/из члена |
Команды предпочитают объединения as const, потому что у них нет рантайм-цены, они сериализуются прозрачно (API шлёт "GET", а не непрозрачный слот enum) и остаются структурными — обычный "GET" откуда угодно просто подходит. enum всё ещё уместен для битовых флагов или когда нужен замкнутый объект-пространство имён в рантайме, но это уже не дефолтный выбор.
▸Почему это работает
Числа по обоим компромиссам. Рантайм: объединение литералов стирается в ничто, тогда как enum компилируется в рантайм-объект — числовой enum испускает IIFE с обратным отображением (Color[Color.Red] = "Red"), который у не-const enum нельзя вытрясти (tree-shake), поэтому каждый стоит примерно 150–400 байт отгружаемого JS там, где объединение литералов стоит ноль. На приложении с ~30 enum статусов/ролей/методов это однозначные килобайты мёртвого веса, которые с as const вы просто не платите. Написание: у литеральной стороны своя цена — узость жертвует переиспользуемостью. as const на большом конфиге делает каждый член readonly, поэтому передача его во что-либо, типизированное изменяемым массивом ((x: string[]) => void), падает с readonly 'a'[] is not assignable to string[], а глубокий as const на большом объекте конфигурации заметно раздувает выведенный тип, который должен держать редактор — на объекте фича-флагов из 200 ключей as const добавил ощутимую задержку подсказки/IntelliSense против написанного руками интерфейса. Компромисс сеньора: тянитесь к точному литералу/as const там, где замкнутое множество — это вся суть (HTTP-методы, уровни логов, конфиг как единый источник истины, из которого вы выводите типы); но когда значению нужно течь в изменяемые, широко переиспользуемые API, readonly-природа литерала становится трением — там расширяйте намеренно (аннотируйте к объединению или уберите as const), обменивая крупицу точности на переиспользуемость.
▸Частая ошибка
Частая оплошность: написать const x = "GET" и предположить, что свойство объекта, который вы из него строите, останется "GET". Примитивный const хранит литерал, но как только он попадает в свойство объекта, свойство снова расширяется до string, если объект не as const. Расширение — про слот, а не про значение-источник.
Каков выведенный тип `tags` в `const t = { tags: ['a', 'b'] } as const`?
Упорядочьте по КОНКРЕТНОСТИ выведенного типа, от самого широкого к самому узкому:
- 1 let x = 'GET' -> string
- 2 const x = 'GET' -> 'GET'
- 3 const o = { m: 'GET' } -> { m: string }
- 4 const o = { m: 'GET' } as const -> { readonly m: 'GET' }
- 01Что такое литеральный тип и почему объединения литералов — предпочтительный лёгкий enum в современном TypeScript?
- 02Почему `const config = { method: 'GET' }` выводит `method` как `string` и какими способами сохранить его как литерал?
- 03Что именно делает `as const` и как он включает конфиг-как-типы?
Литеральные типы закрывают юнит основ, связывая расширение (из урока о выводе) с практической суперсилой: as const превращает рантайм-данные в точные типы. Теперь у вас есть весь базовый слой — структурная типизация, вывод, особые типы any/unknown/never и литералы. Естественный следующий шаг — объединения и пересечения (ваша цель deepensInto в юните 02): объединения литералов были вашим первым вкусом |, и следующий юнит его обобщает — комбинирование типов через | и &, а затем сужение этих объединений обратно к одному члену, ровно тот механизм, что делает unknown пригодным, а never — маркером исчерпания. Теперь, когда увидите Type 'string' is not assignable to type '"GET" | ...', вы сразу знаете: изменяемый слот расширил литерал — и тянетесь за as const прежде всего остального.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.
Примени это
Примени этот урок в реальном проекте.