Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Константні твердження (as const) у TypeScript». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**`as const`** у TypeScript - це const assertion (константне твердження), яке фіксує значення як найточніші літеральні типи та робить всі властивості `readonly` замість розширених примітивів. ```typescript const config = { url: "https://api.example.com", port: 3000 } as const; // Тип: { readonly url: "https://api.example.com"; readonly port: 3000 } ``` **Головне:** лише час компіляції, нульовий вплив на рантайм, глибоке readonly з точними літеральними типами.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**`as const`** - це твердження TypeScript (const assertion), яке змушує компілятор виводити найточніші літеральні типи для значення, роблячи кожну властивість глибоко `readonly` замість розширених примітивів на кшталт `string` чи `number`. ## Теорія ### TL;DR - Без `as const`: TypeScript виводить `string`, `number`, `string[]`. З ним: `"точне значення"`, `42`, `readonly ["a", "b"]`. - Всі властивості стають `readonly`. Масиви перетворюються на фіксовані readonly кортежі. Вкладеність обробляється повністю рекурсивно. - Використовуй для статичних конфігів, констант замість `enum` та ключів React Query. Не використовуй для динамічних даних. - Лише час компіляції. Жодного впливу на JavaScript, що генерується. ### Короткий приклад ```typescript // Без as const - розширені типи const api = { endpoint: "users", version: 1, tags: ["admin", "user"] }; // Тип: { endpoint: string; version: number; tags: string[] } // З as const - літеральні типи скрізь const api = { endpoint: "users", version: 1, tags: ["admin", "user"] } as const; // Тип: { // readonly endpoint: "users"; // readonly version: 1; // readonly tags: readonly ["admin", "user"]; // } ``` Другий об'єкт тепер точний контракт. TypeScript блокує будь-яке перевизначення, ловить помилки в назвах і дає автодоповнення для кожного значення. ### Головна різниця TypeScript за замовчуванням розширює типи, щоб змінні залишались гнучкими під час розробки. Літерал `"users"` стає `string`, бо ти можеш захотіти перевизначити його пізніше. `as const` каже: не розширювати. Кожен примітив залишається точним значенням, і це стосується всіх вкладених об'єктів та масивів рекурсивно. ### Коли використовувати - Статичні конфіги: `as const` дає автодоповнення і запобігає тихим помилкам у значеннях властивостей. - Альтернативи `enum`: виводь union-типи зі значень об'єкта без дублювання двох окремих списків. - Ключі React Query: cache invalidation потребує точних tuple-типів, а не `string[]`. - Тип повернення функції: коли клієнту потрібен `"dark"`, а не просто `string`. - Не використовуй, коли значення приходять з форм, API-відповідей або змінюються в рантаймі. ### Як це обробляє компілятор Під час фази контекстної типізації TypeScript обходить AST рекурсивно і замінює примітивні висновки на `LiteralTypeNode`. Масиви обгортаються в `ReadonlyArray`, об'єкти отримують readonly-обгортки. Після компіляції V8 бачить звичайний JavaScript-об'єкт. Весь контроль існує лише в межах статичного аналізу TypeScript. `as const` не викликає `Object.freeze`. TypeScript заблокує перевизначення `config.port` на рівні типів, але JavaScript все одно може змінити об'єкт у рантаймі. Для справжньої незмінності під час виконання - комбінуй обидва. ### Типові помилки **Очікування незмінності в рантаймі** ```typescript const arr = [1, 2, 3] as const; arr.push(4); // TS Error: Property 'push' does not exist on type 'readonly [1, 2, 3]' // Але JS-масив не заморожений - мутація в рантаймі залишається можливою ``` Рішення: `const arr = Object.freeze([1, 2, 3] as const);` **Застосування `as const` до змінної, що вже розширена** ```typescript const dynamic = "foo"; // вже виведено як string, а не "foo" const config = { key: dynamic } as const; // Тип: { readonly key: string } - літерал втрачено ``` `as const` не може відновити літерал, який вже розширився. Визначай значення прямо в об'єкті або в окремому `const`. **Очікування, що `as const` звужує типи параметрів функції** ```typescript function log(config: { mode: string }) { /* ... */ } log({ mode: "debug" } as const); // Все одно передає `string` - TypeScript перевіряє сигнатуру функції ``` Щоб зберегти літерали в параметрах, використовуй generic: `function log<K extends string>(config: { mode: K })`. ### Де зустрічається - React Query (TanStack): ключі запитів як `readonly ["users"]` кортежі для точного cache matching. - Zod: `z.enum(["admin", "editor"] as const)` для виведення union з масиву без дублювання. - Redux Toolkit: `status: "loading" as const` в payload action для відповідності discriminated union. - Express: конфіги маршрутів з `method: "GET" as const` для типобезпечних таблиць роутингу. ### Питання на співбесіді **Q:** Який тип у `{ a: 1, b: 2 } as const`? **A:** `{ readonly a: 1; readonly b: 2 }`. Обидві властивості - літеральні числові типи, не `number`. **Q:** Яка різниця між `as const` і `Readonly<T>`? **A:** `Readonly<T>` робить властивості readonly, але не звужує до літералів. `{ x: "a" }` з `Readonly` залишить `x: string`. `as const` робить і те, і інше: readonly і літеральні типи. **Q:** Чи можна застосувати `as const` до повернення функції? **A:** Так. `function getConfig() { return { port: 3000 } as const; }` - виклик отримає `readonly port: 3000`, а не `port: number`. **Q:** Наскільки глибоко поширюється readonly? **A:** Повністю рекурсивно. Кожен вкладений об'єкт і масив стають readonly аж до примітивних значень. **Q:** Яка різниця між `as const` і оператором `satisfies` (TypeScript 4.9+)? **A:** `satisfies` перевіряє значення відносно наявного типу, зберігаючи літеральний вивід. `as const` звужує до літералів без такої перевірки. Їх можна поєднувати: `{ port: 3000 } as const satisfies ServerConfig`. **Q:** Навіщо теги в RTK Query потребують `as const`? **A:** Без нього `["Post", post.id]` розширюється до `string[]`. Cache tag matching потребує точних tuple-типів. З `as const` отримуєш `readonly ["Post", string]`, що підходить для матчера. ## Приклади ### Виведення union-типу зі значень об'єкта ```typescript const STATUS = { PENDING: "pending", ACTIVE: "active", INACTIVE: "inactive", } as const; type Status = typeof STATUS[keyof typeof STATUS]; // "pending" | "active" | "inactive" function updateUser(id: string, status: Status) { // TypeScript приймає лише "pending" | "active" | "inactive" } updateUser("123", STATUS.ACTIVE); // ✅ updateUser("123", "deleted"); // ❌ TS Error ``` Тип автоматично оновлюється, коли додаєш або перейменовуєш значення. Не потрібно підтримувати union-визначення вручну в окремому місці. ### Ключі кешу в React Query ```typescript const QUERY_KEYS = { users: ["users"] as const, user: (id: string) => ["user", id] as const, } as const; function useUser(id: string) { return useQuery({ queryKey: QUERY_KEYS.user(id) }); } // Інвалідація працює, бо типи - точні кортежі queryClient.invalidateQueries({ queryKey: QUERY_KEYS.users }); ``` Я бачив, як це ламається в продакшені: хтось прибирає `as const` з файлу ключів, кеш перестає інвалідуватися, і потрібна година щоб відстежити причину. Точні кортежі тут важливі. ### Вкладені об'єкти та розрив між TypeScript і рантаймом ```typescript const config = { db: { host: "localhost", port: 5432, flags: ["ssl", "compression"], }, } as const; // TypeScript блокує це: config.db.port = 5433; // Error: Cannot assign to 'port' because it is a read-only property // Але JavaScript не заважає: (config as any).db.port = 5433; // Жодної помилки в рантаймі ``` Для конфігів, які справді не повинні змінюватись у рантаймі, додавай `Object.freeze`. readonly TypeScript - це статична гарантія. `Object.freeze` JavaScript - це рантаймова. Різні інструменти для різних рівнів.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.