satisfies: проверить, не потеряв вывод

границы≈ 9 минут

Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.

У значения в TypeScript есть два типа сразу: тот, который вывел компилятор, и тот, который потребовали вы. Аннотация : T и утверждение as T вторым затирают первый — переменная становится ровно T, и всё, что компилятор знал о литерале, пропадает. satisfies T проверяет требование и уходит, не трогая вывод.

Разделы темы:·

У аннотации две работы, и вторую замечают не сразу. Первая — сверить значение с типом. Вторая — назначить переменной этот самый тип. Пока тип и значение одинаково подробны, разницы не видно. Она вылезает, когда тип шире значения — а тип почти всегда шире.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

TS2339: Свойство "toUpperCase" не существует в типе "string | RGB" — и вложенной причиной уточнение про RGB. Ошибка честная: palette.primary объявлен как string | RGB, а у кортежа метода toUpperCase нет. Обидно другое: тремя строками выше написано "#0055ff", и компилятор это видел — ровно до того момента, как прочитал аннотацию.

Утверждение as ведёт себя тут точно так же. Отличается оно тем, что не проверяет значение, — об этом была прошлая статья, — но тип переменной оно назначает столь же жёстко.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Ни одной диагностики — и TypeError на исполнении. Ключа accent в объекте нет, но переменная объявлена как Record<Slot, string>, и по этому типу он обязан быть.

«: T» и «as T» одинаково заменяют тип переменной на T. Разница между ними только в том, спрашивают ли они согласия компилятора.

Оператор satisfies ставится там же, где as, — после выражения. Читается он буквально: «это значение удовлетворяет типу». Компилятор сверяет литерал с типом справа и, если всё сошлось, забывает про него: тип переменной остаётся тот, который выведен из самого литерала.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Тишина от компилятора и оба обращения на месте. palette.primary — это string, palette.accent — кортеж [number, number, number]. При этом требование проверено полностью: и набор ключей, и их имена, и типы значений.

Когда значение требованию не отвечает, у satisfies находится собственная диагностика — TS1360. Её больше некому выдать: ни аннотация, ни as такого текста не произносят.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Тип "{ read: number; }" не соответствует ожидаемому типу "Timeouts", а вложенной причиной — какого поля не хватает. Разбирать такие двухэтажные тексты учила статья про чтение диагностики в начале части. Единица в конце — напоминание, что satisfies целиком стирается при сборке: в браузер уехал ровно тот объект, что написан, со своим единственным ключом.

Оператор появился в TypeScript 4.9. До него полноту проверяли отдельной переменной рядом: объявляли её с нужным типом и присваивали ей тот же объект. Приём рабочий, но не равноценный — контекст типа приходит к литералу слишком поздно.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Тип "number[]" не может быть назначен для типа "RGB" — «целевой объект требует следующего числа элементов — 3, но источник может иметь меньше». Объект выводился сам по себе, никакой RGB ему подсказан не был. satisfies в том же примере отработал молча, потому что тип справа участвует в выводе, а не только в проверке.

Самая ходовая аннотация для словаря — Record<string, T>. Она обещает значение по любому ключу, и ровно это обещать не стоило: пока ключи перечислены в литерале рядом, компилятор знает их наизусть, а аннотация заставляет его забыть.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Опечатка в имени маршрута доехала до браузера и превратилась в TypeError. С satisfies ровно тот же объект отвечает иначе.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Свойство "contacts" не существует в типе "{ home: string; about: string; }" — в тексте ошибки видно тот самый тип, который компилятор вывел сам. Требование Record<string, string> при этом проверено: попробуйте написать home: 1, и оно сработает.

Второй подарок — контекстная типизация. Тип справа от satisfies действует и внутрь литерала: параметры функций получают типы оттуда и перестают быть неявным any.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

TS7006 в первом объекте и полная тишина во втором. Ни одной аннотации внутри литерала money не написано — типы параметров пришли из Formatter, а тип переменной остался выведенным из литерала.

«satisfies ничего не огрубляет» — удобная формулировка, но неточная, и проверить это стоит своими руками.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

TS2322: Тип "string" не может быть назначен для типа ""Список"". satisfies не отменяет обычных правил вывода: поле объекта можно переприсвоить, поэтому строка в нём расширяется до string. Контекстный тип тоже говорит string — расширению нечего помешать.

Значит, литералы надо закрепить до проверки — тем самым as const из главы про объекты. Порядок слов строго такой: сперва заморозить, потом сверить.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Литерал сохранился, а единственная жалоба — TS2540 на присваивание в readonly-поле, и та не мешает записи выполниться: типы до браузера не доезжают. Заодно видно, что делает каждое слово. as const отвечает за точность и неизменяемость, satisfies — за полноту: убери из объекта board, и один as const смолчит, а пара выдаст TS1360.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

TS1355: Утверждение "const" может применяться только к ссылкам на элементы перечисления либо к строковым, числовым, логическим литералам, литералам массивов или объектов. Перевёрнутый порядок даже не собирается — и это удобно: перепутать не выйдет молча.

«as const satisfies T» читается справа налево: сперва заморозить литерал, потом сверить его с T. Наоборот не соберётся.

ЗаписьПроверяет значениеТип у переменнойКогда уместна
const x: T = …ДаРовно Tзначение должно быть взаимозаменяемо с любым другим T
const x = … as TНет, только «достаточно похоже»Ровно Tграница с чужими данными, и то с оговорками из прошлой статьи
const x = … satisfies TДаВыведенный из литералаконфиги, словари, таблицы — всё, что пишут один раз и потом читают

Правило выбора короткое: если значение потом раздают наружу как «какой-нибудь T» — берите аннотацию. Если это конкретный объект, к которому будут обращаться по конкретным полям, — satisfies. as в этом ряду не соперник: он единственный ничего не проверяет.

Точность, впрочем, не всегда благо. Иногда широкий тип из аннотации нужен именно потому, что он широкий.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

TS2367: Это сравнение кажется непреднамеренным, поскольку типы ""dev"" и ""prod"" не перекрываются. Компилятор прав по-своему: он видит константу "dev" и не знает, что ветка написана на будущее. Замените satisfies AppConfig на : AppConfig — поле снова станет "dev" | "prod", и жалоба исчезнет. Это тот случай, когда аннотация уместнее.

satisfies — оператор над выражением, а не часть объявления. Написать let config satisfies AppConfig; без значения нельзя: проверять там нечего. Зато его можно поставить на любое выражение — например, на возвращаемое значение, — и точный тип доедет до вызывающей стороны.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Аннотация возврата function makeConfig(): Config дала бы ту же проверку, но и то же огрубление: config.mode стал бы "dev" | "prod", и присваивание в mode не прошло бы.

  • Аннотация : T и утверждение as T одинаково заменяют тип переменной на T; отличаются они только тем, проверяют ли значение.
  • satisfies T сверяет значение с типом и оставляет переменной тип, выведенный из самого литерала.
  • У несоответствия при satisfies свой код — TS1360, с вложенной причиной внутри.
  • satisfies сохраняет точный набор ключей и раздаёт контекстные типы внутрь литерала — параметрам колбэков в том числе.
  • Строковые литералы satisfies сам по себе не удержит: нужен порядок as const satisfies T, обратный не собирается.
  • Аннотация всё ещё нужна там, где значение должно остаться «одним из»: иначе компилятор начнёт ругаться на сравнения, написанные на будущее.

1/4Объект { primary: "#0055ff", accent: [255, 200, 0] } записан с аннотацией : Record<Slot, string | RGB>. Что скажет компилятор на palette.primary.toUpperCase()?

2/4Чем satisfies T отличается от as T?

3/4Какой тип будет у titles.list после const titles = { list: "Список" } satisfies Record<"list", string>;?

4/4Почему as const пишут перед satisfies, а не после?

Вернуть методы значениям

Словарь правил валидации проверен аннотацией — и из-за неё же перестал работать. Верните обращение к test, не меняя ни тип Rule, ни сам объект: правьте только связку между ними.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Аннотация огрубила оба поля до Rule. Меняем её на satisfies: проверка сохранится, а типы значений останутся такими, какими компилятор их вывел из литерала.

tsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Требование никуда не делось: уберите поле name — и получите TS1360 о том, что его не хватает.

Развернуть ложь

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

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

as меняем на satisfies — и ложь превращается в диагностику.

tsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

TS1360: Тип "{ save: string; open: string; }" не соответствует ожидаемому типу "Record<Icon, string>", вложенной причиной — «свойство "close" отсутствует… и является обязательным». Ровно то, о чём as предпочёл промолчать.

Заморозить и сверить

Подписи вкладок нужны сразу с двумя гарантиями: покрыты все значения Tab и точные строки не потеряны. Добейтесь того, чтобы пример собирался без единой диагностики, и проверьте себя: убрав из объекта calendar, вы должны получить ошибку.

ts
Tab добавляет отступ. Чтобы выйти из редактора, нажмите Escape — фокус перейдёт на кнопку запуска.
страница примера

Одного as const мало: он удержит литералы, но не проверит полноту. Одного satisfies тоже мало: он проверит полноту, но литералы расширятся до string. Нужны оба, и в этом порядке.

tsлистинг · не запускается
Листинг только для чтения: этот код не запускается в браузере.

Уберите calendar — появится TS1360. Переставьте слова местами (satisfies … as const) — появится TS1355.