satisfies: проверить, не потеряв вывод
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
У значения в TypeScript есть два типа сразу: тот, который вывел компилятор, и тот, который потребовали вы. Аннотация : T и утверждение as T вторым затирают первый — переменная становится ровно T, и всё, что компилятор знал о литерале, пропадает. satisfies T проверяет требование и уходит, не трогая вывод.
У аннотации две работы, и вторую замечают не сразу. Первая — сверить значение с типом. Вторая — назначить переменной этот самый тип. Пока тип и значение одинаково подробны, разницы не видно. Она вылезает, когда тип шире значения — а тип почти всегда шире.
TS2339: Свойство "toUpperCase" не существует в типе "string | RGB" — и вложенной причиной уточнение про RGB. Ошибка честная: palette.primary объявлен как string | RGB, а у кортежа метода toUpperCase нет. Обидно другое: тремя строками выше написано "#0055ff", и компилятор это видел — ровно до того момента, как прочитал аннотацию.
Утверждение as ведёт себя тут точно так же. Отличается оно тем, что не проверяет значение, — об этом была прошлая статья, — но тип переменной оно назначает столь же жёстко.
Ни одной диагностики — и TypeError на исполнении. Ключа accent в объекте нет, но переменная объявлена как Record<Slot, string>, и по этому типу он обязан быть.
«: T» и «as T» одинаково заменяют тип переменной на T. Разница между ними только в том, спрашивают ли они согласия компилятора.
Оператор satisfies ставится там же, где as, — после выражения. Читается он буквально: «это значение удовлетворяет типу». Компилятор сверяет литерал с типом справа и, если всё сошлось, забывает про него: тип переменной остаётся тот, который выведен из самого литерала.
Тишина от компилятора и оба обращения на месте. palette.primary — это string, palette.accent — кортеж [number, number, number]. При этом требование проверено полностью: и набор ключей, и их имена, и типы значений.
Когда значение требованию не отвечает, у satisfies находится собственная диагностика — TS1360. Её больше некому выдать: ни аннотация, ни as такого текста не произносят.
Тип "{ read: number; }" не соответствует ожидаемому типу "Timeouts", а вложенной причиной — какого поля не хватает. Разбирать такие двухэтажные тексты учила статья про чтение диагностики в начале части. Единица в конце — напоминание, что satisfies целиком стирается при сборке: в браузер уехал ровно тот объект, что написан, со своим единственным ключом.
Оператор появился в TypeScript 4.9. До него полноту проверяли отдельной переменной рядом: объявляли её с нужным типом и присваивали ей тот же объект. Приём рабочий, но не равноценный — контекст типа приходит к литералу слишком поздно.
Тип "number[]" не может быть назначен для типа "RGB" — «целевой объект требует следующего числа элементов — 3, но источник может иметь меньше». Объект выводился сам по себе, никакой RGB ему подсказан не был. satisfies в том же примере отработал молча, потому что тип справа участвует в выводе, а не только в проверке.
Самая ходовая аннотация для словаря — Record<string, T>. Она обещает значение по любому ключу, и ровно это обещать не стоило: пока ключи перечислены в литерале рядом, компилятор знает их наизусть, а аннотация заставляет его забыть.
Опечатка в имени маршрута доехала до браузера и превратилась в TypeError. С satisfies ровно тот же объект отвечает иначе.
Свойство "contacts" не существует в типе "{ home: string; about: string; }" — в тексте ошибки видно тот самый тип, который компилятор вывел сам. Требование Record<string, string> при этом проверено: попробуйте написать home: 1, и оно сработает.
Второй подарок — контекстная типизация. Тип справа от satisfies действует и внутрь литерала: параметры функций получают типы оттуда и перестают быть неявным any.
TS7006 в первом объекте и полная тишина во втором. Ни одной аннотации внутри литерала money не написано — типы параметров пришли из Formatter, а тип переменной остался выведенным из литерала.
«satisfies ничего не огрубляет» — удобная формулировка, но неточная, и проверить это стоит своими руками.
TS2322: Тип "string" не может быть назначен для типа ""Список"". satisfies не отменяет обычных правил вывода: поле объекта можно переприсвоить, поэтому строка в нём расширяется до string. Контекстный тип тоже говорит string — расширению нечего помешать.
Значит, литералы надо закрепить до проверки — тем самым as const из главы про объекты. Порядок слов строго такой: сперва заморозить, потом сверить.
Литерал сохранился, а единственная жалоба — TS2540 на присваивание в readonly-поле, и та не мешает записи выполниться: типы до браузера не доезжают. Заодно видно, что делает каждое слово. as const отвечает за точность и неизменяемость, satisfies — за полноту: убери из объекта board, и один as const смолчит, а пара выдаст TS1360.
TS1355: Утверждение "const" может применяться только к ссылкам на элементы перечисления либо к строковым, числовым, логическим литералам, литералам массивов или объектов. Перевёрнутый порядок даже не собирается — и это удобно: перепутать не выйдет молча.
«as const satisfies T» читается справа налево: сперва заморозить литерал, потом сверить его с T. Наоборот не соберётся.
| Запись | Проверяет значение | Тип у переменной | Когда уместна |
|---|---|---|---|
const x: T = … | Да | Ровно T | значение должно быть взаимозаменяемо с любым другим T |
const x = … as T | Нет, только «достаточно похоже» | Ровно T | граница с чужими данными, и то с оговорками из прошлой статьи |
const x = … satisfies T | Да | Выведенный из литерала | конфиги, словари, таблицы — всё, что пишут один раз и потом читают |
Правило выбора короткое: если значение потом раздают наружу как «какой-нибудь T» — берите аннотацию. Если это конкретный объект, к которому будут обращаться по конкретным полям, — satisfies. as в этом ряду не соперник: он единственный ничего не проверяет.
Точность, впрочем, не всегда благо. Иногда широкий тип из аннотации нужен именно потому, что он широкий.
TS2367: Это сравнение кажется непреднамеренным, поскольку типы ""dev"" и ""prod"" не перекрываются. Компилятор прав по-своему: он видит константу "dev" и не знает, что ветка написана на будущее. Замените satisfies AppConfig на : AppConfig — поле снова станет "dev" | "prod", и жалоба исчезнет. Это тот случай, когда аннотация уместнее.
satisfies — оператор над выражением, а не часть объявления. Написать let config satisfies AppConfig; без значения нельзя: проверять там нечего. Зато его можно поставить на любое выражение — например, на возвращаемое значение, — и точный тип доедет до вызывающей стороны.
Аннотация возврата 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, ни сам объект: правьте только связку между ними.
Аннотация огрубила оба поля до Rule. Меняем её на satisfies: проверка сохранится, а типы значений останутся такими, какими компилятор их вывел из литерала.
Требование никуда не делось: уберите поле name — и получите TS1360 о том, что его не хватает.
Развернуть ложь
Здесь as обещал компилятору три ключа, а в объекте их два. Компилятор молчит, браузер падает. Замените одно слово так, чтобы ошибку было видно до запуска, и прочитайте, что именно скажет компилятор.
as меняем на satisfies — и ложь превращается в диагностику.
TS1360: Тип "{ save: string; open: string; }" не соответствует ожидаемому типу "Record<Icon, string>", вложенной причиной — «свойство "close" отсутствует… и является обязательным». Ровно то, о чём as предпочёл промолчать.
Заморозить и сверить
Подписи вкладок нужны сразу с двумя гарантиями: покрыты все значения Tab и точные строки не потеряны. Добейтесь того, чтобы пример собирался без единой диагностики, и проверьте себя: убрав из объекта calendar, вы должны получить ошибку.
Одного as const мало: он удержит литералы, но не проверит полноту. Одного satisfies тоже мало: он проверит полноту, но литералы расширятся до string. Нужны оба, и в этом порядке.
Уберите calendar — появится TS1360. Переставьте слова местами (satisfies … as const) — появится TS1355.