Avatar ·

Что означает "as const" в TypeScript и когда его использовать?

Привет, коллеги! Я начал изучать TypeScript недавно и наткнулся на конструкцию `as const`. Честно говоря, я не совсем понимаю, что она делает и зачем она нужна. Вот пример, который я нашел в документации: ```typescript const colors = ['red', 'green', 'blue'] as const; ``` Я понимаю, что `as` используется для приведения типа (type assertion), например, `someValue as string`. Но что значит добавить `const` после `as`? Чем это отличается от простого объявления массива или объекта? Я попробовал написать такой код: ```typescript const fruits = ['apple', 'banana']; const fruitsConst = ['apple', 'banana'] as const; // В чем разница между fruits и fruitsConst? // Почему я не могу изменить fruitsConst? ``` Также я заметил, что когда я использую `as const` с объектами, TypeScript начинает строго следить за значениями свойств. Например, если я пишу: ```typescript const config = { mode: 'development' as const, port: 3000 }; ``` То TypeScript не дает мне присвоить `config.mode = 'production'`, хотя раньше это работало. Объясните, пожалуйста, в чем тут магия? И самое главное — какие реальные сценарии использования у этой конструкции? Где это действительно полезно, а где лучше не применять? Буду благодарен за объяснение на пальцах, с примерами кода. Заранее спасибо! <<>>

Что такое as const и как он работает?

as const — это специальная конструкция в TypeScript, которая называется const assertion (константное утверждение). Она говорит компилятору: "Считай, что это значение никогда не изменится, и выведи максимально точный тип". В отличие от обычного as, который просто меняет тип, as const делает тип более строгим и неизменяемым.

Основные эффекты as const

  • Литеральные типы — значения становятся литеральными (например, 'hello' вместо string)
  • Неизменяемость (readonly) — массивы и объекты помечаются как readonly
  • Глубокий вывод — эффект применяется рекурсивно ко всем вложенным элементам

Примеры с пояснениями

1. Массивы

// Обычный массив
const fruits = ['apple', 'banana'];
// Тип: string[]
// Можно изменять: fruits.push('orange') — работает

// С as const
const fruitsConst = ['apple', 'banana'] as const;
// Тип: readonly ["apple", "banana"]
// Нельзя изменять: fruitsConst.push('orange') — ошибка!
// Каждый элемент имеет точный литеральный тип

2. Объекты

// Обычный объект
const config = {
  mode: 'development',
  port: 3000
};
// Тип: { mode: string; port: number }
// Можно менять: config.mode = 'production' — работает

// С as const
const configConst = {
  mode: 'development' as const,
  port: 3000
};
// Тип: { mode: "development"; port: 3000 }
// Нельзя менять: configConst.mode = 'production' — ошибка!
// port: number (так как 3000 — число, а не литерал)

Реальные сценарии использования

Сценарий 1: Enum-подобные объекты

const Status = {
  SUCCESS: 'success',
  ERROR: 'error',
  PENDING: 'pending'
} as const;

type StatusType = typeof Status[keyof typeof Status];
// StatusType = "success" | "error" | "pending"

function handleStatus(status: StatusType) {
  // Работает только с указанными значениями
}

handleStatus('success');  // OK
handleStatus('invalid');  // Ошибка!

Сценарий 2: Строгая типизация конфигураций

const API_ROUTES = {
  users: '/api/users',
  posts: '/api/posts',
  comments: '/api/comments'
} as const;

function fetchData(route: typeof API_ROUTES[keyof typeof API_ROUTES]) {
  return fetch(route);
}

fetchData('/api/users');     // OK
fetchData('/api/unknown');   // Ошибка!

Сценарий 3: Кортежи с фиксированной длиной

// Без as const — это просто массив
const pair = ['x', 1]; // тип: (string | number)[]

// С as const — это кортеж
const tuple = ['x', 1] as const; // тип: readonly ["x", 1]

function processPair(pair: readonly [string, number]) {
  const [str, num] = pair;
  // str: string, num: number
}

processPair(tuple); // OK
processPair(pair);  // Ошибка! pair может быть любой длины

Когда НЕ стоит использовать as const

  • Если вам нужно изменять массив или объект (добавлять/удалять элементы)
  • Если вы работаете с динамическими данными, которые могут меняться
  • В случаях, когда широкая типизация (string, number) предпочтительнее

as const — мощный инструмент для создания безопасных и точных типов. Используйте его, когда хотите гарантировать, что значение останется неизменным, и TypeScript будет строго следить за этим на этапе компиляции.

<<>>

Альтернативный взгляд: as const как способ создания константных контекстов

Вместо того чтобы думать о as const как о приведении типа, представьте его как способ создания "константного контекста" для значения. Это особенно полезно при работе с условными типами и маппингом.

// Пример с функциями
function createAction(type: string, payload?: any) {
  return { type, payload } as const;
}

const action = createAction('LOGIN', { user: 'Alice' });
// Тип: { readonly type: string; readonly payload: { user: string } }

// Но лучше так:
function createAction2<T extends string, P>(type: T, payload: P) {
  return { type, payload } as const;
}

const action2 = createAction2('LOGIN', { user: 'Alice' });
// Тип: { readonly type: "LOGIN"; readonly payload: { user: string } }

Практический совет: Используйте as const для создания дискриминированных юнионов (discriminated unions). Это основа для TypeScript-редьюсеров в Redux или для работы с состоянием в React.

type Action =
  | { type: 'ADD_TODO'; text: string }
  | { type: 'REMOVE_TODO'; id: number }
  | { type: 'TOGGLE_TODO'; id: number };

// Без as const вам пришлось бы писать type: 'ADD_TODO' as const
// в каждом объекте, что неудобно

Если вам нужна изменяемость, но при этом точные литеральные типы, рассмотрите использование Object.freeze() на уровне выполнения, а as const оставьте для типов. Но помните: as const работает только на этапе компиляции и не влияет на рантайм.