Skip to main content

Константні твердження (as const) у TypeScript

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 - це рантаймова. Різні інструменти для різних рівнів.

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

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

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

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