open atlas
↑ К треку
Система типов TypeScript вглубь TS · 01 · 04

Литеральные типы: одиночные значения как типы и as const, который их хранит

Литеральный тип — это одно значение как тип: "GET", 200, true. Объединения литералов — лёгкие enum. const хранит литералы, let их расширяет, а as const фиксирует глубокие readonly-литералы на объектах, массивах и кортежах — приём, питающий конфиг-как-типы.

TS Junior ◷ 13 min
Уровень
ОсновыJuniorMiddleSenior

Вы определяете объект конфигурации метода { 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. 1 let x = 'GET' -> string
  2. 2 const x = 'GET' -> 'GET'
  3. 3 const o = { m: 'GET' } -> { m: string }
  4. 4 const o = { m: 'GET' } as const -> { readonly m: 'GET' }
Вспомните перед уходом
  1. 01
    Что такое литеральный тип и почему объединения литералов — предпочтительный лёгкий enum в современном TypeScript?
  2. 02
    Почему `const config = { method: 'GET' }` выводит `method` как `string` и какими способами сохранить его как литерал?
  3. 03
    Что именно делает `as const` и как он включает конфиг-как-типы?
Итог

Литеральные типы закрывают юнит основ, связывая расширение (из урока о выводе) с практической суперсилой: as const превращает рантайм-данные в точные типы. Теперь у вас есть весь базовый слой — структурная типизация, вывод, особые типы any/unknown/never и литералы. Естественный следующий шаг — объединения и пересечения (ваша цель deepensInto в юните 02): объединения литералов были вашим первым вкусом |, и следующий юнит его обобщает — комбинирование типов через | и &, а затем сужение этих объединений обратно к одному члену, ровно тот механизм, что делает unknown пригодным, а never — маркером исчерпания. Теперь, когда увидите Type 'string' is not assignable to type '"GET" | ...', вы сразу знаете: изменяемый слот расширил литерал — и тянетесь за as const прежде всего остального.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 5 завершено
Связанные уроки

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

Примени это

Примени этот урок в реальном проекте.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.