Перейти к содержанию

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

Тема курса уже описана двумя строковыми литералами. Теперь используем точные значения для действий интерфейса и подписей его вариантов. Нам нужны фильтрация по теме и выбор порядка. У этих действий разные параметры, поэтому один объект с несколькими необязательными полями легко позволил бы бессмысленные сочетания.

Литеральный тип обозначает конкретное значение. Объединение таких значений описывает конечный набор, а as const помогает сохранить точность исходного литерала. Дополнительно satisfies проверяет соответствие конфигурации нужной форме, сохраняя её конкретные свойства. Результат урока — два ясно различимых действия и полная таблица подписей трёх режимов темы.

Действие определяется своим параметром

Полный src/app.ts снимка lesson-14:

import type { TopicFilter } from './model.js';

const topicLabels = {
  all: 'Все темы', frontend: 'Фронтенд', publishing: 'Публикация'
} as const satisfies Record<TopicFilter, string>;

type SortMode = 'original' | 'lessons';
type Action =
  | { readonly type: 'filter'; readonly topic: TopicFilter }
  | { readonly type: 'sort'; readonly mode: SortMode };

function describeAction(action: Action): string {
  if (action.type === 'filter') return `Тема: ${topicLabels[action.topic]}`;
  return `Порядок: ${action.mode}`;
}

const requested = { type: 'filter', topic: 'frontend' } as const;
console.log(describeAction(requested));
console.log(describeAction({ type: 'sort', mode: 'lessons' }));

Ожидаются строки «Тема: Фронтенд» и «Порядок: lessons». Это описание действий, а не обработчик DOM и не изменение карточек. Модель Course, четыре записи и парсер сохраняются в снимке; файл сосредоточен на новом договоре команд интерфейса.

Для действия filter обязателен topic, для действия sort обязателен mode. Поле type имеет разные точные литералы и позволяет отделить варианты. В первой ветке describeAction доступна тема; после её раннего возврата остаётся вариант сортировки, поэтому доступен mode.

Если бы мы написали { type: string, topic?: TopicFilter, mode?: SortMode }, форма разрешила бы действие без нужного параметра и действие с обоими параметрами. После проверки произвольной строки тип не связал бы обязательность поля с выбранным действием так же ясно. Наша модель отражает конечный набор допустимых сочетаний.

Это всё ещё статическая связь. Внешний объект с type: 'filter' и отсутствующим topic не становится правильным по факту существования Action. При приёме данных требуется исполняемая проверка, как в шестом уроке. Для локальных исходных литералов тип позволяет заметить такое несоответствие заранее.

Сохранить точный литерал

У переменной requested поля помечены через as const. Они сохраняют точные значения filter и frontend, а поля имеют договор readonly. Поэтому весь объект подходит одному конкретному варианту Action без отдельной аннотации.

Обычный объект без такого уточнения допускает изменение своих полей. Вывод может расширить строковые свойства до string, чтобы разрешить будущие записи. Это удобно для обычного изменяемого объекта, но не выражает точную конечную команду. Можно решить задачу явной аннотацией const requested: Action = ...; выбор зависит от того, какую информацию хотите сохранить у самой переменной.

as const не вызывает Object.freeze. При выполнении это тот же объект, а скрытые мутации через иной alias остаются вопросом владения данными. Конструкция помогает проверке исходника, но не превращает память браузера в неизменяемое хранилище.

Имена действий тоже имеют смысл. Значение filter описывает операцию, а frontend — её параметр. Не следует объединять их в одну строку вроде filterFrontend, если приложение действительно имеет общее действие выбора темы. И наоборот, самостоятельную операцию нельзя бездумно маскировать как ещё одну тему только ради повторного использования типа.

Полная таблица подписей

topicLabels должен иметь ключи all, frontend и publishing, а значения — строки. Record<TopicFilter, string> задаёт именно эту конечную форму. Если забыть подпись одного режима или написать опечатку ключа в свежем литерале, соответствие нарушится.

Конструкция satisfies проверяет форму, не заменяя полученный тип выражения целиком на указанный справа. Вместе с as const она сохраняет конкретные строковые подписи. Свойства механизма описаны в заметках TypeScript 4.9; текущая версия курса поддерживает этот приём.

Таблица является настоящим объектом JavaScript, в отличие от одного объявления TopicFilter. Поэтому её можно читать во время выполнения. Она не обязана быть единственным источником всех тем приложения: наш тип также используется моделью данных. Если список изменяется динамически с сервера, статический конечный набор уже потребует другой архитектуры.

Рассмотрим неправильный фрагмент для чтения:

// Не включайте в app.ts: у sort нет нужного mode.
const broken: Action = { type: 'sort', topic: 'frontend' };

Здесь наличие темы не заменяет режим сортировки. Такой объект описывает неправильную форму действия, даже если оба строковых значения сами по себе знакомы системе. Проверка полезна именно потому, что связывает поля по смыслу варианта.

Конечный набор и расширение курса

Если позже добавится режим title в SortMode, это изменит допустимые параметры сортировки. Если добавится третье действие reload, надо обновить функции, которые рассматривают варианты. Простая функция из этого урока использует две ветки; в следующем уроке мы введём явный способ замечать забытый вариант при исчерпывающем разборе.

При этом изменение типа не реализует новый алгоритм. Добавить литерал title недостаточно, чтобы каталог начал сортировать названия. Программа должна иметь исполняемый обработчик нового режима, а текст урока — ожидаемый результат. Тип описывает возможность, реализация исполняет действие.

Для подписей важно не перепутать машинное значение и отображаемый текст. В данных остаётся frontend, на странице можно показать «Фронтенд». Замена языка интерфейса не должна менять устойчивые идентификаторы темы. Такой договор облегчает будущую локализацию и сохранение состояния в URL.

Конфигурация и команды имеют разный срок жизни

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

В нашем фрагменте действие сортировки описано строкой lessons. Это машинный режим, а не окончательная русская подпись интерфейса. При создании настоящей формы для него понадобится своя таблица подписей. TypeScript поможет согласовать конечные ключи, но выбор текста и доступность формы останутся задачами представления.

Если определить конфигурацию только как Record<string, string>, любой строковый ключ будет формально допустим, и забытая подпись publishing не станет таким же явным несоответствием полного конечного набора. Узкий TopicFilter поэтому важен не меньше, чем оператор satisfies. Проверка полезна, когда проверяемая форма сама выражает нужное правило.

Для локальной команды не следует применять as const к уже неизвестному внешнему объекту, будто это проверяет его происхождение. Приём работает с информацией исходного выражения и не подтверждает фактические поля входа. Когда значение получено из URL, строки сначала проверяются относительно допустимого набора, а затем создаётся новый объект действия. Так каждый этап имеет собственную ответственность и точные литералы продолжают означать действительную проверенную возможность, а не удобное утверждение автора.

Проверка не должна превращаться в множество утверждений as Action возле каждого вызова. Если типы регулярно приходится обходить, сначала посмотрите, соответствует ли модель реальному набору действий. Возможно, источник ещё не проверен или конкретное значение было чрезмерно расширено при выводе.

Исходники этого урока подготовлены без выполнения. Ожидаемые две строки показывают различие параметров команд, а не работу настоящих элементов формы. В следующем уроке применим тот же принцип к состояниям каталога и отделим готовые данные от загрузки и ошибки.

Оглавление курса.