Перейти к основному содержимому

Литеральные типы в TypeScript

Литеральный тип — это тип, который допускает ровно одно значение — например строку "north" или число 1 — вместо любой строки или любого числа.


Как понимать литеральные типы в TypeScript

Литеральные типы позволяют точно указать, какие значения может хранить переменная. Они точнее, чем широкие типы string, number или boolean.

Они являются строительными блоками для точного и типобезопасного кода.

Ключевые понятия:

  • Строковые литералы: конкретные строковые значения, например "success" | "error"
  • Числовые литералы: конкретные числа, например 1 | 2 | 3
  • Логические литералы: либо true, либо false
  • Шаблонные литеральные типы: строковые литеральные типы, построенные с помощью синтаксиса шаблонных строк

Типичные случаи применения:

  • Определение фиксированного набора допустимых значений
  • Создание размеченных объединений
  • Типобезопасная обработка событий
  • Типизация ответов API
  • Объекты конфигурации

Строковые литеральные типы

Строковый литеральный тип обозначает одно конкретное строковое значение. Обычно несколько таких типов объединяют с помощью | в объединение допустимых значений.

Пример

direction может хранить только одну из четырёх строк. Любая другая строка, например "northeast", вызывает ошибку компиляции — как при присваивании переменной, так и при вызове move.

Prompt
Покажите строковый литеральный тип-объединение, используемый для переменной и для параметра функции. Объявите direction с типом "north" | "south" | "east" | "west", присвойте "north", а затем "south", и оставьте закомментированными присваивания "northeast" и "up" вместе с ошибками, которые они бы вызвали. Напишите функцию move, принимающую то же литеральное объединение и выводящую Движение <direction>, вызовите её с "east" и оставьте move("up") закомментированным как недопустимый вызов.
Вывод: программа на TypeScript и захваченный вывод

Числовые литеральные типы

Так же как и строковые литералы, числовые литеральные типы обозначают конкретные числовые значения.

Пример

diceRoll принимает только числа от 1 до 6. Поскольку Math.random() возвращает обычный number, функции rollDice нужно утверждение типа, чтобы пообещать, что она возвращает один из шести литералов. Выпавшее значение меняется при каждом запуске.

Prompt
Покажите числовой литеральный тип-объединение для значения игральной кости. Объявите diceRoll с типом 1 | 2 | 3 | 4 | 5 | 6, присвойте 1, а затем 6, и оставьте закомментированными присваивания 0, 7 и 2.5 вместе с их ошибками. Напишите rollDice(), возвращаемым типом которой является то же объединение и которая возвращает Math.floor(Math.random() * 6) + 1, приведённое к этому объединению, затем выведите Выпало: <result> (число случайно при каждом запуске).
Вывод: программа на TypeScript и захваченный вывод

Логические литеральные типы

Логические литеральные типы используются реже, поскольку логических значений всего два, но в некоторых случаях они полезны — например чтобы указать, что функция всегда возвращает true.

Пример

alwaysSucceed обязана вернуть литерал true. SuccessFlag и FailureFlag смешивают логические, строковые и числовые литералы, а processResult различает их с помощью простых проверок на равенство.

Prompt
Покажите логические литеральные типы сами по себе и в сочетании с другими литералами. Объявите type YesOnly = true и функцию alwaysSucceed(), возвращаемым типом которой является литерал true. Затем объявите SuccessFlag = true | "success" | 1 и FailureFlag = false | "failure" | 0, а также функцию processResult, принимающую любой из них и выводящую Операция выполнена успешно, когда значение равно true, "success" или 1, и Операция не выполнена в противном случае. Вызовите её с true, "success", 1 и false.
Вывод: программа на TypeScript и захваченный вывод

Литеральные типы в объектах

Литеральные типы можно использовать как типы свойств внутри типов объектов. Это даёт очень конкретные формы объектов.

Пример

HTTPSuccess и HTTPError ограничивают status и statusText несколькими литеральными значениями каждый. Обычная проверка диапазона status не сужает объединение, поэтому в примере используется небольшой защитник типа isSuccess; внутри каждой ветки после этого можно безопасно использовать data или error.

Prompt
Покажите литеральные типы, используемые как типы свойств внутри типов объектов, для моделирования HTTP-ответов. Объявите HTTPSuccess (status: 200 | 201 | 204, statusText: "OK" | "Created" | "No Content", data: any) и HTTPError (status: 400 | 401 | 403 | 404 | 500, соответствующие литералы statusText, error: string), а также HTTPResponse как их объединение. Напишите защитник типа isSuccess (response is HTTPSuccess), который проверяет, что статус находится в диапазоне 200, и функцию handleResponse, которая с его помощью выводит Успех: <statusText> и данные при успехе, а иначе выводит Ошибка <status>: <statusText> и Сообщение: <error>. Вызовите её с ответом 200 "OK", содержащим { username: "john_doe", email: "[email protected]" }, и с ответом 404 "Not Found" с ошибкой Пользователь не найден в базе данных.
Вывод: программа на TypeScript и захваченный вывод

Шаблонные литеральные типы

Начиная с TypeScript 4.1 вы можете строить новые строковые литеральные типы, комбинируя существующие с помощью синтаксиса шаблонных строк — обратных кавычек и подстановок ${...}. Каждая комбинация значений из объединений-подстановок становится членом нового объединения.

Пример

Пример идёт от простого объединения DirectionAndDistance к более продвинутым применениям: Capitalize для построения имён событий, отображаемый тип, создающий имена геттеров, infer для извлечения имён параметров из строки маршрута, тип CssValue из числа и единицы измерения, а также типизированный построитель SQL-запросов. Файл заканчивается export {}, чтобы его локальный тип EventTarget не конфликтовал со встроенным типом DOM.

Prompt
Покажите шаблонные литеральные типы, построенные из строковых литеральных объединений, от простых к продвинутым. Объедините объединения Direction и Distance в DirectionAndDistance (`${Direction}-${Distance}`) и присвойте переменной route допустимые маршруты, например "north-5km". Затем постройте EventName с помощью Capitalize, отображаемый тип GetterName<T>/UserGetters над типом User, рекурсивный ExtractRouteParams<T>, который извлекает имена :param из пути с помощью infer, тип CssValue вида `${number}${CssUnit}`, тип ApiUrl и функцию query<T extends Table>, возвращаемым типом которой является `SELECT * FROM ${T}${string}` и которая вызывается для таблицы users с WhereCondition. Недопустимые присваивания оставьте закомментированными. Ничего не выводится. Добавьте export {}; в конце, чтобы локальный тип EventTarget не конфликтовал со встроенным глобальным типом DOM.
Вывод: программа на TypeScript и захваченный вывод

Рекомендуемые практики

Делайте так:

  • Используйте литеральные типы для фиксированных наборов значений (перечислимые варианты, значения конфигурации)
  • Комбинируйте их с типами-объединениями для большей типобезопасности
  • Используйте шаблонные литеральные типы для сопоставления строковых шаблонов
  • По возможности позволяйте выводу типов работать за вас
  • Документируйте, что означает каждый литеральный тип

Не делайте так:

  • Не злоупотребляйте литеральными типами там, где лучше подошёл бы более общий тип
  • Не создавайте чрезмерно большие типы-объединения, которые замедляют проверку типов
  • Не используйте строковые литералы там, где лучше подошло бы перечисление

Вопросы производительности

  • Большие типы-объединения могут замедлять проверку типов
  • Сложные шаблонные литеральные типы могут увеличивать время компиляции
  • Используйте псевдонимы типов, чтобы давать имена сложным литеральным типам, вместо их повторения
  • Помните об ограничениях глубины рекурсии в TypeScript для рекурсивных шаблонных литеральных типов