Skip to main content

Оператор satisfies у TypeScript

satisfies - перевіряє, що значення відповідає типу під час компіляції, зберігаючи при цьому вузький виведений тип. TypeScript 4.9 додав цей оператор, щоб вирішити конкретну проблему: анотація : Type розширює значення до широкого типу і стирає конкретну інформацію, яку TypeScript вже знає.

Теорія

TL;DR

  • : Type присвоює широкий тип змінній і втрачає деталі; satisfies Type перевіряє відповідність, але зберігає вузький виведений тип
  • Аналогія: анотація типу переформовує значення під оголошений тип; satisfies перевіряє форму, не змінюючи саме значення
  • Використовуй satisfies, коли потрібна валідація І можливість викликати .toUpperCase() на рядку або .map() на конкретному масиві після перевірки
  • Нульовий вплив на runtime - повністю стирається у JS
  • Правило вибору: об'єкт зі змішаними типами значень, до яких потрібен специфічний доступ після валідації? Використовуй satisfies

Швидкий приклад

typescript
type Colors = Record<string, string | number[]>; // Анотація типу: розширює до string | number[], деталі губляться const colors: Colors = { red: "#ff0000", green: [0, 255, 0] }; colors.red.toUpperCase(); // ❌ string | number[] не має toUpperCase // satisfies: перевіряє, потім зберігає вузький виведений тип const colors2 = { red: "#ff0000", green: [0, 255, 0] } satisfies Colors; colors2.red.toUpperCase(); // ✅ TypeScript виводить string colors2.green.map(x => x * 2); // ✅ TypeScript виводить number[]

Перевірка типу виконується, ловить будь-яку невідповідність, а потім TypeScript використовує вузький виведений тип для всього, що йде далі.

Головна різниця

: Type присвоює Type змінній. З цього моменту TypeScript бачить тільки string | number[] і забуває, що red конкретно є string. satisfies виконує ту саму перевірку, але повертає оригінальний вузький виведений тип назад в оголошення. Однакова безпека на етапі компіляції, різний результат далі по коду.

Коли використовувати

  • Об'єкт потребує валідації, але після неї ти звертаєшся до конкретних властивостей: використовуй satisfies
  • as const занадто вузький (readonly literals) і потрібна гнучка валідація: використовуй satisfies
  • Об'єкти конфігурації, теми, таблиці маршрутів, константи кодів стану: все це підходить
  • Прості примітиви (5 satisfies number): пропускай, виведення вже впорається само
  • Об'єкт більше п'яти властивостей і розбивати на окремо типізовані поля незручно: використовуй satisfies замість анотацій

Порівняння підходів

ПідхідПеревіряє?Зберігає вузький тип?Примітка
const x: Type = valueТакНі (розширює до Type)Стандартна анотація
const x = value as TypeНіНіПропускає всі перевірки
const x = { ... } as constНіТак (readonly)Занадто вузький для більшості випадків
const x = { ... } satisfies TypeТакТакВалідація + виведення
Коли використовуватиОбчислювані значення, типи поверненняПрості readonly літералиСкладні об'єкти з валідацією

Як це обробляє компілятор

Перевірник типів TypeScript призначає виведений тип значення тимчасовому вузлу, перевіряє, чи присвоюється він цільовому типу, а потім повертає оригінальний вузький виведений тип назад в оголошення. Жодного коду в runtime не генерується. Використовується структурна типізація (structural subtyping) - той самий механізм, що й implements у класах. Скомпільований JavaScript ідентичний звичайному літералу об'єкта.

Типові помилки

Очікування, що satisfies заповнить відсутні властивості

typescript
// Неправильно: satisfies перевіряє точну відповідність, а не заповнює пропуски const x = {} satisfies { a: number } & { b: string }; // ❌ Помилка: відсутні a і b // Правильно: надай всі необхідні властивості const x = { a: 1, b: "hi" } satisfies { a: number } & { b: string }; // ✅

Використання на примітивах

typescript
// Безглуздо - TypeScript вже виводить 5 як number const n = 5 satisfies number;

Це не додає жодної цінності й збиває з пантелику тих, хто читає код.

Очікування звуження всередині тіла функції

typescript
// satisfies перевіряє тільки сигнатуру, не те що відбувається всередині const fn = ((x: string) => x.length) satisfies (x: string) => number; // Тип повернення перевіряється, але змінні closure не звужуються

Якщо потрібне звуження самої функції, використовуй as const.

Вкладені об'єкти втрачають глибину виведення

satisfies перевіряє структуру верхнього рівня. Вкладені об'єкти валідуються, але не завжди звужуються так глибоко, як можна очікувати. Один граничний випадок: { children: [] } satisfies Tree виводить children як never[], бо порожній масив не має типу елементів. Застосовуй satisfies ближче до місця де споживаєш значення або використовуй рекурсивний тип.

Де зустрічається в реальних проектах

  • Next.js: const metadata = { title: "App" } satisfies Metadata - перевірка статичних exports, зберігає рядковий літерал для обробки og:title
  • TanStack Query: const defaultOptions = { queries: { retry: 3 } } satisfies DefaultOptions - зберігає числовий літерал для порівнянь при перевизначенні
  • tRPC: валідація структур роутерів із збереженням виведення типів на рівні процедур
  • Zod: const config = { ... } satisfies z.infer<typeof schema> - перевірка структури зі збереженням літеральних значень за замовчуванням

Особисто я найбільше використовую цей патерн у файлах конфігурації з більш ніж п'ятьма властивостями. В такому випадку розбивати все на окремо типізовані поля стає незручно, а as const додає readonly скрізь, де воно не потрібне.

Питання на співбесіді

Q: У чому різниця між satisfies, as const і type assertion?
A: satisfies перевіряє і зберігає вузькі типи. as const зберігає вузькі типи без перевірки. Type assertion (as Type) взагалі пропускає перевірку - це приведення, а не перевірка.

Q: Чи впливає satisfies на runtime?
A: Ні. Це тільки compile-time, повністю стирається у відкомпільованому JavaScript. Вивід ідентичний звичайному літералу.

Q: Чи можна поєднувати satisfies з as const?
A: Так: { ... } as const satisfies Type. Це дає readonly literal типи плюс перевірку відповідності цільовому типу. Порядок важливий - спочатку as const, потім satisfies.

Q: Як відтворити satisfies у проекті на TypeScript 4.8?
A: Через допоміжний тип: type Satisfies<T, U extends T> = U. Потім const x = { ... } as Satisfies<Colors, typeof x>. Незручно, але покриває більшість випадків до оновлення на 4.9+.

Q: Чому б не завжди використовувати satisfies замість анотацій?
A: Анотації підходять для обчислюваних значень, типів повернення функцій і випадків де широкий тип потрібен споживачам далі по коду. satisfies потребує літерального значення для виведення типу - він не може допомогти зі значенням, якого ще немає в момент оголошення.

Приклади

Базовий: палітра кольорів

typescript
type Colors = Record<string, string | number[]>; const palette = { red: "#ff0000", green: [0, 255, 0], blue: "#0000ff", } satisfies Colors; // ✅ TypeScript знає, що red і blue - рядки palette.red.startsWith("#"); // працює palette.blue.toUpperCase(); // працює // ✅ TypeScript знає, що green - number[] palette.green.map(channel => channel * 0.5); // працює

Якщо замість satisfies використати Colors як анотацію, всі три властивості стають string | number[] і жоден з цих викликів методів не компілюється без type guard.

Середній рівень: конфігурація маршрутів

typescript
type Route = { path: string; method: "GET" | "POST" | "PUT" | "DELETE"; handler: string; }; const routes = { getUsers: { path: "/api/users", method: "GET", handler: "UserController.getAll", }, createUser: { path: "/api/users", method: "POST", handler: "UserController.create", }, } satisfies Record<string, Route>; // TypeScript знає точні ключі type RouteKeys = keyof typeof routes; // "getUsers" | "createUser" // І точні літерали методів routes.getUsers.method; // "GET", а не "GET" | "POST" | "PUT" | "DELETE" // Валідація спрацьовує при помилці в назві методу const bad = { home: { path: "/", method: "PATCH", handler: "HomeController" }, } satisfies Record<string, Route>; // ❌ Помилка: "PATCH" не присвоюється типу "GET" | "POST" | "PUT" | "DELETE"

Цей патерн часто зустрічається в Express middleware та конфігурації Next.js App Router. Ти отримуєш безпеку типів від анотації, не втрачаючи літеральні типи, потрібні для логіки маршрутизації або switch-виразів.

Просунутий рівень: поєднання з as const

typescript
const palette = { red: "#ff0000", green: "#00ff00", blue: "#0000ff", } as const satisfies Record<string, `#${string}`>; // Значення readonly літерали І перевірені як рядки формату hex-кольору type PaletteKey = keyof typeof palette; // "red" | "green" | "blue" palette.red; // "#ff0000" (readonly літерал, не просто string) // Валідація ловить порушення формату під час компіляції const badPalette = { yellow: "ffff00", // Без # } as const satisfies Record<string, `#${string}`>; // ❌ Помилка: "ffff00" не присвоюється типу `#${string}`

as const робить значення readonly літералами. satisfies перевіряє, що вони відповідають цільовому формату. Разом вони дають перевірену, повністю типізовану константну карту без жодних накладних витрат у runtime.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?