12k
All articles

Практическое руководство по оператору `satisfies` в TypeScript

Пояснение оператора TypeScript satisfies: примеры конфигов, узкий вывод типов и когда использовать его вместо as или аннотации через двоеточие.

OpenReplay Team
OpenReplay Team
Практическое руководство по оператору `satisfies` в TypeScript

Оператор satisfies проверяет значение на соответствие типу, не изменяя выведенный тип самого значения — таким образом вы одновременно получаете безопасность типа и точность вывода. Это единственное поведение устраняет конкретную повседневную проблему: аннотация через двоеточие на объекте конфигурации защищает от неверных значений, но уничтожает литеральные ключи и узкие типы, которые вы хотели сохранить. В этом руководстве представлены концептуальная модель, канонический пример и правило выбора между satisfies, as и обычной аннотацией через двоеточие. Все примеры написаны для TypeScript 4.9 и выше.

Ключевые выводы

  • satisfies проверяет значение на соответствие типу, сохраняя узкий выведенный тип значения, поэтому автодополнение и сужение литералов продолжают работать.
  • При аннотации через двоеточие побеждает объявленный тип, и значение расширяется до него; при использовании satisfies побеждает значение, а тип используется только для его проверки.
  • as не проверяет ваше значение — он переопределяет проверку типов, именно поэтому const user = {} as User компилируется без ошибок, но выбрасывает исключение во время выполнения при обращении к user.name.
  • Одновременное использование аннотации и satisfies (const x: T = {…} satisfies T) избыточно: двоеточие имеет приоритет и сводит на нет сужение типов, которого вы добивались.
  • satisfies работает только на этапе компиляции и не генерирует JavaScript-код; для данных, поступающих из сети или файла, используйте валидатор времени выполнения, например Zod.

Какую проблему решает satisfies

Существуют два паттерна, которые подталкивают разработчиков к использованию satisfies. Первый — аннотация через двоеточие на объекте с ключами, которая расширяет тип и уничтожает автодополнение. Пример с routes от Мэтта Покока наглядно это демонстрирует: аннотируйте тип как Record<string, {}>, и вы сможете обращаться к любому ключу, даже несуществующему, без какой-либо ошибки.

const routes: Record<string, {}> = {
  "/": {},
  "/users": {},
  "/admin/users": {},
};
routes.awdkjanwdkjn; // Нет ошибки — тип теперь Record<string, {}>

Второй паттерн — свойство с типом-объединением. В примере от freeCodeCamp свойство, типизированное как объединение строкового литерала и объекта, не позволяет вызывать строковые методы без явной проверки.

type Info = "John" | "Jack" | { id: number; age: number };
type Person = { myInfo: Info; myOtherInfo: Info };

const applicant: Person = { myInfo: "John", myOtherInfo: { id: 123, age: 22 } };
applicant.myInfo.toUpperCase();
// Property 'toUpperCase' does not exist on type 'Info'

В итоге перед каждым обращением приходится писать if (typeof applicant.myInfo === "string"). Обе проблемы имеют общую причину: аннотация через двоеточие заменила конкретный тип значения более широким объявленным типом.

Как работает оператор satisfies?

satisfies проверяет, что выражение соответствует типу, не изменяя тип, который TypeScript выводит для него. Оператор был представлен в TypeScript 4.9, выпущенном 15 ноября 2022 года, и его поведение идентично в текущей стабильной версии TypeScript 6.0 и релиз-кандидате 7.0. Поскольку порт 7.0 на Go сохранил семантику проверки типов структурно идентичной версии 6.0, satisfies применяет те же самые правила в новом компиляторе.

Концептуальная модель в формулировке Покока: при аннотации через двоеточие тип побеждает значение; при использовании satisfies значение побеждает тип. Когда вы используете satisfies, TypeScript выводит максимально узкий возможный тип и использует аннотацию только для его проверки. Оператор работает исключительно на этапе компиляции — он не генерирует JavaScript-код и не несёт никаких накладных расходов во время выполнения, поэтому может обнаружить опечатку или неверный тип значения ещё до запуска кода.

Канонический пример использования satisfies

Решение состоит в том, чтобы перенести аннотацию от двоеточия к завершающему satisfies: обе проблемы исчезают одновременно — вы сохраняете узкие литеральные типы и по-прежнему получаете ошибку при неверном значении.

const routes = {
  "/": {},
  "/users": {},
  "/admin/users": {},
} satisfies Record<string, {}>;

routes.awdkjanwdkjn;
// Property 'awdkjanwdkjn' does not exist on type
// '{ "/": {}; "/users": {}; "/admin/users": {}; }'

Автодополнение для routes теперь показывает реальные пути. Валидация по-прежнему работает: присвойте значение, которое запрещает аннотация, и компилятор отклонит его.

const routes = {
  "/": null, // Type 'null' is not assignable to type '{}'
} satisfies Record<string, {}>;

Тот же приём решает проблему с объединением типов. applicant.myInfo сужается до литерала "John", поэтому .toUpperCase() допустим без какой-либо проверки:

const applicant = {
  myInfo: "John",
  myOtherInfo: { id: 123, age: 22 },
} satisfies Person;

applicant.myInfo.toUpperCase(); // OK — выведен как "John"

satisfies vs as vs аннотация через двоеточие

as не проверяет ваше значение — он переопределяет систему проверки типов, именно поэтому const user = {} as User компилируется без ошибок, а затем выбрасывает исключение во время выполнения в момент обращения к user.name. В этом состоит принципиальное различие между тремя инструментами:

ИнструментПроверяет значение?Сохраняет узкий вывод?Можно солгать TS?Использовать когда
: Type (двоеточие)ДаНет — расширяет до типаНетНамеренно нужен более широкий тип
satisfies TypeДаДаНетНужна валидация и узкий вывод
as TypeНетН/ДДаПочти никогда по умолчанию

Опасность во время выполнения вполне конкретна:

type User = { id: string; name: { first: string; last: string } };
const user = {} as User;
user.name.first; // Нет ошибки в IDE — выбрасывает исключение во время выполнения

as также незаметно деградирует. Следующий код компилируется сегодня, но стоит добавить одно обязательное поле в User, и defaultUser станет невалидным без какой-либо ошибки:

type User = { id: string; name: string };
const defaultUser = { id: "123", name: "Matt" } as User;

Именно такой класс ошибок призван выявлять session replay: вы видите реальную форму объекта в момент, когда обращение к свойству выбросило исключение, а не ту форму, которую вы задекларировали. Замените as на satisfies, и компилятор немедленно укажет на отсутствующее поле.

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

Ловушка избыточной аннотации

Одновременное использование аннотации и satisfiesconst joe: TUser = {…} satisfies TUser — избыточно: аннотация через двоеточие имеет приоритет и незаметно сводит на нет сужение типов, которое должен был обеспечить satisfies. Статья на Refine.dev показывает последствия: обращение к вложенному свойству завершается ошибкой, потому что победил объявленный тип и внутреннее сужение было отброшено. Выберите что-то одно. Если вам нужно сужение — уберите двоеточие.

Где satisfies действительно полезен

Используйте satisfies для типизированных конфигураций, словарей на основе Record и значений дискриминированных объединений, которые вы хотите сохранить узкими. Карты тем и палитр — типичный пример: официальный пример с palette проверяет каждую RGB-запись, сохраняя при этом литеральные типы для каждого ключа. Для опциональных ключей оберните запись в Partial, чтобы отсутствующие ключи были допустимы, а присутствующие по-прежнему проверялись:

type Keys = "id" | "name" | "email" | "age";

const person = {
  id: 12345,
  name: "Jacky",
  email: "jacky@test.com",
} satisfies Partial<Record<Keys, string | number>>;

person.name.toUpperCase(); // сужен до string

Когда не стоит использовать satisfies

Не используйте satisfies для простого объекта, где аннотация : Type уже говорит всё необходимое. Не используйте его, когда вам нужен более широкий тип — если вы планируете переприсваивать переменную позже, satisfies заблокирует это, поскольку фиксирует узкий выведенный тип:

// аннотация через двоеточие — переприсваивание допустимо
let id: string | number = "123";
id = 456; // OK

// satisfies — значение побеждает, поэтому тип сужен до string
let id2 = "123" satisfies string | number;
id2 = 456; // Type 'number' is not assignable to type 'string'

И полностью откажитесь от него для данных, которые вы не контролируете. satisfies никогда не выполняется в рантайме, поэтому не может проверить JSON-payload или данные из формы во время выполнения — для данных, пересекающих границу сети или файловой системы, используйте валидатор схемы времени выполнения, например Zod или io-ts.

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

Часто задаваемые вопросы

Работает ли оператор satisfies в JavaScript-файлах с JSDoc?

Да. TypeScript 5.0 добавил JSDoc-тег @satisfies, который делает для JavaScript-файлов то же самое, что оператор satisfies делает для TypeScript-файлов. В JavaScript-файле тег указывается над объявлением — например, аннотация @satisfies с именем типа — и проверяющий механизм валидирует значение на соответствие этому типу, сохраняя узкий выведенный тип. Это позволяет проектам на JavaScript с JSDoc-типизацией получить те же преимущества валидации с сужением типов без перехода на .ts-файлы.

Добавляет ли satisfies накладные расходы во время выполнения или генерирует дополнительный JavaScript?

Нет. satisfies — это исключительно компиляционный оператор уровня типов, который не генерирует JavaScript-код, поэтому не несёт никаких накладных расходов во время выполнения и не влияет на размер бандла. Ключевое слово и следующий за ним тип стираются при компиляции, точно так же, как аннотация через двоеточие. Поскольку ничего не выполняется в рантайме, оператор также не может проверять данные во время выполнения — именно поэтому для сетевых payload'ов или пользовательского ввода по-прежнему необходим валидатор схемы времени выполнения, например Zod.

Почему мой объект по-прежнему не проходит проверку типов при одновременном использовании аннотации через двоеточие и satisfies?

Потому что аннотация через двоеточие всегда имеет приоритет, и конструкция satisfies становится мёртвым синтаксисом. Запись const config: Theme = {…} satisfies Theme означает, что побеждает объявленный тип Theme, значение расширяется до Theme, и узкий вывод, который должен был сохранить satisfies, отбрасывается. Обращение к вложенному литеральному свойству затем завершается ошибкой, как если бы satisfies отсутствовал. Уберите двоеточие и оставьте только завершающий satisfies для сохранения сужения типов.

Когда следует использовать Zod вместо satisfies для валидации данных?

Используйте Zod или другой валидатор схемы времени выполнения, например io-ts, всякий раз, когда данные поступают в рантайме из источника, который вы не контролируете: сетевого ответа, JSON-файла или данных формы. satisfies проверяет только литералы, которые вы пишете в исходном коде на этапе компиляции, и не генерирует код времени выполнения, поэтому не может инспектировать неизвестные входящие данные. Используйте satisfies для типизированных конфигураций, Record-словарей и значений объединений в собственном коде; используйте Zod для внешних границ.

Open-source session replay

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Star on GitHub12k

We use cookies to improve your experience. By using our site, you accept cookies.