Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Оператор satisfies у TypeScript». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**`satisfies`** - перевіряє відповідність значення типу під час компіляції, зберігаючи вузький виведений тип. На відміну від `: Type`, не розширює. ```typescript const colors = { red: "#ff0000", green: [0, 255, 0] } satisfies Colors; colors.red.toUpperCase(); // ✅ string, а не string | number[] ``` **Ключове:** перевіряє без розширення. Нульовий вплив на runtime.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**`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.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.