Литеральные типы и as const: точные значения без лишних аннотаций
Автор: Казачкин Даниил Михайлович · Обновлено
Литеральный тип описывает конкретное значение: не произвольную строку, а, например, 'compact'. Такая точность нужна для режима отображения, команды или единицы измерения, но её…
Литеральный тип описывает конкретное значение: не произвольную строку, а, например, 'compact'. Такая точность нужна для режима отображения, команды или единицы измерения, но её легко потерять при сохранении значения в изменяемый объект.
Цель и необходимые знания
Предполагается знакомство с объектами и псевдонимами типов. В этом уроке вы научитесь предсказывать расширение литерала до общего типа, выводить допустимые значения из массива и отличать as const от замораживания объекта во время выполнения. Основные примеры самодостаточны и рассчитаны на TypeScript 6 с включённым strict.
Почему const не делает всё точным
Объявление const mode = 'compact' запрещает переприсвоить саму переменную, поэтому компилятор может сохранить конкретное строковое значение. Но const settings = { mode: 'compact' } запрещает только замену ссылки. Свойство объекта всё ещё можно изменить. Чтобы не запрещать обычные присваивания других строк, вывод часто расширяет тип такого свойства до string. Это объяснимое решение компилятора, а не потеря данных в самом объекте.
Проверьте различие между массивом допустимых режимов и изменяемой настройкой. Кортеж с as const предоставляет конечное множество значений; индексирование типом number извлекает тип любого его элемента. У полученного Mode нет отдельной копии списка строк, поэтому опечатку не придётся исправлять в двух местах.
export {};
const modes = ['compact', 'comfortable'] as const;
type Mode = (typeof modes)[number];
function rowHeight(mode: Mode): number {
return mode === 'compact' ? 28 : 44;
}
const mutableSettings = { mode: 'compact' };
const exactSettings = { mode: 'compact' } as const;
console.log(rowHeight(exactSettings.mode));
console.log(modes.join(', '));
if (false) {
// @ts-expect-error Свойство изменяемого объекта имеет тип string.
rowHeight(mutableSettings.mode);
// @ts-expect-error Такого значения нет в исходном кортеже режимов.
const unsupported: Mode = 'dense';
void unsupported;
}Вывод — 28 и compact, comfortable. Обратите внимание: из-за широкой аннотации объекта не произошло никакой ошибки вычисления в рантайме. Ошибка возникла раньше, потому что функцию разрешено вызывать только с известными вариантами. Один правильный способ исправления — объявить изменяемую настройку как { mode: Mode }. Другой — оставить точный неизменяемый литерал там, где настройки действительно константны.
Три способа сохранить нужную информацию
Явная аннотация подходит, когда переменная должна позже принимать любой вариант выбранного объединения. as const удобно для таблицы значений, ключей меню или набора размеров, которые создаются вместе с кодом. Отдельное литеральное поле удобно для дискриминатора: по значению kind программа будет отличать разные формы сообщения. Эти варианты решают разные задачи, поэтому не нужно заменять ими друг друга механически.
Константное утверждение влияет на TypeScript-представление литерала: значения становятся точнее, свойства — readonly, массивы — readonly-кортежами. Оно не вызывает Object.freeze. Если рядом существует изменяемая ссылка на тот же вложенный объект, изменения через эту ссылку могут остаться возможными. При необходимости реального запрета изменений следует отдельно продумать копирование, владение объектом или механизм замораживания.
Проверка членства вместо утверждения
Строка из файла или пользовательского ввода не превращается в Mode от желания разработчика. input as Mode скрывает вопрос, содержится ли значение в списке. Когда вариантов немного, обычная функция с проверками равенства прозрачно описывает нужное условие. Результат проверки позволяет продолжить работу с конечным набором значений, не заставляя потребителей принимать любую строку.
export {};
type Mode = 'compact' | 'comfortable';
function parseMode(value: string): Mode | undefined {
if (value === 'compact' || value === 'comfortable') return value;
return undefined;
}
console.log(parseMode('compact'));
console.log(parseMode('COMPACT'));Первая строка вывода — compact, вторая — undefined: регистр является частью выбранного формата. Если продукт допускает ввод без учёта регистра, нормализацию нужно сделать осознанно до проверки. Это уже изменение поведения, а не уточнение аннотации. В большом приложении полезно иметь одно место, которое отвечает за допустимые варианты и правила их распознавания.
Типичные ошибки
Часто пытаются применить as const к уже вычисленной переменной вместо литерала и ожидают восстановить потерянное множество значений. Но типовая точность не берётся из истории исполнения произвольной функции. Другая ошибка — передавать readonly-кортеж функции, которая требует изменяемый массив и вправе его менять. Исправьте контракт функции, если она только читает коллекцию; не снимайте защиту без причины.
Ещё одна ловушка — объявить каждый пользовательский идентификатор литералом конкретного тестового значения. Для статуса это полезное ограничение, для открытого набора идентификаторов — случайная привязка к фикстуре. Выбирайте точность по договору: перечисление отражает известный замкнутый набор, а string — произвольную допустимую строку, которую при необходимости проверяет отдельное правило.
Практикум: единицы длительности
Создайте константный массив ['minute', 'hour'], получите из него тип единицы и напишите функцию преобразования в минуты. Передайте значение 2 с каждой единицей; ожидаются 2 и 120. Затем попробуйте передать 'day' и добейтесь ошибки компилятора. Наконец, опишите изменяемый объект настройки так, чтобы единицу можно было переключать между двумя вариантами, но не записывать любую строку.
Разбор: список единиц фиксируется через as const, тип извлекается из элементов массива, а поле изменяемой настройки аннотируется этим типом. Делать весь объект настройки константным не нужно, поскольку пользователь действительно меняет единицу. Внешнее значение сначала проверяется, как в функции parseMode. Если добавляется день, нужно расширить исходный набор и расчёт: один лишь новый литерал не гарантирует, что формула уже учитывает третий вариант. Для такой гарантии следующий урок об объединениях использует исчерпывающую обработку.
Частые вопросы
Нужно ли всегда писать as const после объекта?
Нет. При редактировании формы часто нужен изменяемый объект с более широкими типами свойств. Фиксируйте литералы, когда точное значение или позиция элемента являются частью контракта, а не просто наблюдаемым состоянием объекта в момент его создания.
Шаблонный строковый тип проверит реальную дату?
Тип наподобие шаблона с несколькими числовыми частями описывает допустимую форму строки на уровне типов, но не календарные ограничения. Он не знает число дней конкретного месяца и не разбирает внешний текст при исполнении. Для этого потребуется отдельная функция проверки даты.