Оператор satisfies у TypeScript
satisfies - перевіряє, що значення відповідає типу під час компіляції, зберігаючи при цьому вузький виведений тип. TypeScript 4.9 додав цей оператор, щоб вирішити конкретну проблему: анотація : Type розширює значення до широкого типу і стирає конкретну інформацію, яку TypeScript вже знає.
Теорія
TL;DR
: Typeприсвоює широкий тип змінній і втрачає деталі;satisfies Typeперевіряє відповідність, але зберігає вузький виведений тип- Аналогія: анотація типу переформовує значення під оголошений тип;
satisfiesперевіряє форму, не змінюючи саме значення - Використовуй
satisfies, коли потрібна валідація І можливість викликати.toUpperCase()на рядку або.map()на конкретному масиві після перевірки - Нульовий вплив на runtime - повністю стирається у JS
- Правило вибору: об'єкт зі змішаними типами значень, до яких потрібен специфічний доступ після валідації? Використовуй
satisfies
Швидкий приклад
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 заповнить відсутні властивості
// Неправильно: satisfies перевіряє точну відповідність, а не заповнює пропуски
const x = {} satisfies { a: number } & { b: string };
// ❌ Помилка: відсутні a і b
// Правильно: надай всі необхідні властивості
const x = { a: 1, b: "hi" } satisfies { a: number } & { b: string }; // ✅Використання на примітивах
// Безглуздо - TypeScript вже виводить 5 як number
const n = 5 satisfies number;Це не додає жодної цінності й збиває з пантелику тих, хто читає код.
Очікування звуження всередині тіла функції
// 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 потребує літерального значення для виведення типу - він не може допомогти зі значенням, якого ще немає в момент оголошення.
Приклади
Базовий: палітра кольорів
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.
Середній рівень: конфігурація маршрутів
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
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.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.