open atlas
↑ К треку
Система типов TypeScript вглубь TS · 06 · 04

Оператор satisfies

Как `satisfies` валидирует значение против типа, не расширяя его — сохраняя точный выведенный тип, который аннотация выбросила бы, — в сравнении с аннотацией и `as`, плюс где всё ещё нужен контекстный тип.

TS Senior ◷ 14 min
Уровень
ОсновыJuniorMiddleSenior

У вас есть таблица маршрутов: { home: "/", profile: "/u/:id" }. Вы хотите сразу двух вещей: чтобы компилятор проверил, что каждое значение — корректная строка пути, и чтобы он запомнил точные литеральные ключи, так что routes.home типизирован как "/", а не string. Аннотация даёт проверку, но выбрасывает литералы. Каст хранит литералы, но не делает реальной проверки. Годами приходилось выбирать. satisfies — это оператор, дающий и то и другое.

Три инструмента на одном значении

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

Прогоните один и тот же объект через каждый инструмент и смотрите, что станет с его типом.

type Routes = Record<string, `/${string}`>;

// 1) Аннотация — валидирует, но РАСШИРЯЕТ до аннотированного типа
const a: Routes = { home: "/", profile: "/u/:id" };
a.home;
// ^? `/${string}`   — литерал "/" потерян; a.home теперь просто «строка пути»
// a.dashboard;      // нет ошибки — ключи Record<string, ...>, полнота не проверяется

// 2) as — хранит тип, который вы утверждаете, но НЕ делает реальной проверки
const b = { home: "/", dashboard: "oops" } as Routes;
//                     ^^^^^^^^ опечатка + невалидный путь, но `as` это заглушает
b.home;
// ^? `/${string}`   — тоже расширен, и баг проскользнул

// 3) satisfies — валидирует И сохраняет точный выведенный тип
const c = { home: "/", profile: "/u/:id" } satisfies Routes;
c.home;
// ^? "/"            — точный литерал сохранён
c.profile;
// ^? "/u/:id"
// const d = { home: "x" } satisfies Routes;
// Error: Type 'string' is not assignable to type `/${string}` — невалидный путь пойман

Аннотация расширяет: тип переменной становится Routes, поэтому a.home забывает, что был "/". Каст лжёт: as делает проверку присваиваемости лишь в одну сторону и подавляет ошибки лишних свойств и значений — dashboard: "oops" проходит. satisfies валидирует, не расширяя: значение проверяется против Routes, но переменная сохраняет свой выведенный тип { home: "/"; profile: "/u/:id" }, так что каждый литерал выживает.

И проверка полноты, и точные типы по каждому ключу

Канонический выигрыш — запись, где вы хотите проверить набор ключей и сохранить точные типы значений по ключам. Используйте тип-ограничение для формы и satisfies для сохранения вывода:

type Color = "red" | "green" | "blue";

const palette = {
  red: [255, 0, 0],
  green: [0, 255, 0],
  blue: [0, 0, 255],
} satisfies Record<Color, [number, number, number]>;

// Полнота: уберите ключ — и получите ошибку
// const p2 = { red: [255,0,0], green: [0,255,0] } satisfies Record<Color, [number,number,number]>;
// Error: Property 'blue' is missing.

palette.red;
// ^? [number, number, number]   — сохранён как кортеж, а не number[]
palette.red[0].toFixed(2);
// ok — элемент — `number`, доступен, потому что тип-кортеж выжил
const keys = Object.keys(palette);
// у значения по-прежнему точные ключи, поэтому нижестоящий код может на них опираться

С простой аннотацией const palette: Record<Color, ...> palette.red был бы расширен, и вы потеряли бы точность кортеж-против-массива и точный набор ключей. satisfies сохраняет оба: ограничение валидирует полноту и форму значений; выведенный тип хранит литералы и кортежи.

Почему это работает

Почему satisfies вообще не меняет тип? Потому что это утверждение соответствия только на уровне системы типов, а не приведение. Тип выражения вычисляется обычным выводом (точным для литералов без as const вплоть до обычных правил расширения), а satisfies T просто добавляет ограничение «этот выведенный тип должен быть присваиваем T» как проверку на этапе компиляции. Если проверка прошла, выведенный тип выходит наружу неизменным. Это инверсия аннотации, которая навязывает T как тип, а не проверяет против него.

Ловушка: satisfies не даёт контекстный тип

Единственное место, где satisfies не может заменить аннотацию, — контекстная типизация. Аннотация на переменной протекает своим типом внутрь, давая неаннотированным параметрам функций их типы. satisfies проверяет только наружу, после того как значение уже выведено, поэтому он не засевает параметры.

type Handlers = { onClick: (e: MouseEvent) => void };

// Аннотация: тип протекает внутрь, поэтому `e` типизирован контекстно
const h1: Handlers = {
  onClick: (e) => { e.clientX; },   // e: MouseEvent — выведен из аннотации
};

// satisfies: проверка ПОСЛЕ вывода, поэтому `e` не получает контекстного типа
const h2 = {
  onClick: (e) => { e.clientX; },
  //         ^ Error: Parameter 'e' implicitly has an 'any' type.
} satisfies Handlers;

В h2 параметр стрелки e выводится до того, как satisfies запускает свою проверку, поэтому контекстно типизировать его нечем — при noImplicitAny это ошибка; вы должны сами аннотировать e: MouseEvent. Правило: берите аннотацию, когда нужно, чтобы целевой тип управлял выводом параметров колбэков; берите satisfies, когда хотите валидировать значение, сохранив его точный выведенный тип. Они дополняют друг друга, а не взаимозаменяемы.

Викторина

Для `const cfg = { port: 8080 } satisfies Record<string, number>` каков тип `cfg.port` и почему?

Закончи аналогию

Представьте три инструмента как способы обработать посылку на таможне. Аннотация перепаковывает всё в стандартную коробку (вы проходите досмотр, но теряете оригинальную упаковку). `as` пропускает с поддельным штампом (без реального досмотра). `satisfies` — это инспектор, который сверяет содержимое с правилами, а затем ___ — именно поэтому ваши литеральные типы выживают.

Вспомните перед уходом
  1. 01
    Сравните аннотацию, `as` и `satisfies` на одном объектном литерале: что каждый делает с типом и с безопасностью?
  2. 02
    Когда `satisfies` НЕ может заменить аннотацию и почему?
Итог

satisfies (TS 4.9) проверяет значение против типа, не расширяя его, сохраняя точный выведенный тип, который аннотация выбросила бы, и реальную валидацию, которую as пропускает. Три инструмента образуют ясную таблицу: аннотация проверяет, но расширяет (литералы потеряны, полнота на записях не проверяется), as хранит утверждённый тип, но не делает реальной проверки (баги проходят), а satisfies и валидирует, и сохраняет узкий выведенный тип — идеален для палитр, таблиц маршрутов и записей конфигурации, где нужны проверка полноты плюс точные типы значений по ключам. Его единственное ограничение — контекстная типизация: так как он работает после вывода, он не может засеять неаннотированные параметры колбэков, поэтому берите аннотацию, когда целевой тип должен управлять выводом. На этом юнит про функции и this закрыт; вы теперь владеете разрешением перегрузок, типизацией и стиранием this, потоком управления на основе утверждений и точным различием между аннотацией, кастом и satisfies. Теперь, когда встретишь as SomeType в объекте конфигурации или таблице маршрутов, спроси себя: не даст ли satisfies ту же безопасность с бонусом — литеральные типы выживут ниже по коду.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 8 завершено
Связанные уроки

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources1
expand
  1. 01

Trademarks belong to their respective owners. Editorial reference only.