Avatar ·

Что означает "as const" в TypeScript и для чего это нужно?

Всем привет! Я начал изучать TypeScript и наткнулся на конструкцию `as const`. Понял, что она как-то связана с утверждением типов, но не могу разобраться до конца. Вот пример, который меня смущает: ```typescript const colors = ['red', 'green', 'blue'] as const; // typeof colors -> readonly ["red", "green", "blue"] const myColor = 'red' as const; // typeof myColor -> "red" (литеральный тип, а не string) const config = { api: 'https://api.example.com', port: 3000 } as const; // typeof config -> { readonly api: "https://api.example.com"; readonly port: 3000 } ``` Я вижу, что типы меняются, но не понимаю: 1. Почему просто `const colors = ['red', 'green', 'blue']` даёт тип `string[]`, а с `as const` — кортеж с конкретными литералами? 2. Зачем вообще нужно указывать `as const`, если переменная и так объявлена через `const`? Разве `const` не делает значение неизменяемым? 3. В каких реальных сценариях это может пригодиться? Видел, что используют в Redux actions, но не до конца понял механику. Буду благодарен за подробное объяснение с примерами! <<>>

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

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

Давайте разберёмся на примерах:

1. Литеральные типы вместо общих

Без as const:

const color = 'red';
// typeof color -> string (общий тип)

С as const:

const color = 'red' as const;
// typeof color -> "red" (конкретный литеральный тип)

Почему так? Обычно TypeScript расширяет типы: если вы присваиваете строку переменной с const, он понимает, что значение не изменится, но тип всё равно выводится как string. as const говорит: "Нет, это именно строка 'red', а не любая строка".

2. Массивы становятся кортежами

// Без as const
const arr1 = ['red', 'green', 'blue'];
// typeof arr1 -> string[] (может быть любой длины)

// С as const
const arr2 = ['red', 'green', 'blue'] as const;
// typeof arr2 -> readonly ["red", "green", "blue"] (фиксированный кортеж)

Это полезно, когда вам нужно гарантировать, что массив содержит именно эти элементы в этом порядке.

3. Объекты становятся глубоко неизменяемыми

const config = {
  api: 'https://api.example.com',
  port: 3000
} as const;

// Все поля readonly, типы — литералы:
// config.api -> readonly "https://api.example.com"
// config.port -> readonly 3000

Почему as const, если есть const?

Это частая путаница. const в JavaScript/TypeScript — это привязка переменной. Она означает, что вы не можете переназначить переменную:

const x = 5;
x = 10; // Ошибка! Нельзя переназначить const

Но содержимое объекта или массива можно изменить:

const obj = { a: 1 };
obj.a = 2; // Это работает!
obj.b = 3; // И это тоже

as const же делает глубокую неизменяемость на уровне типов (читай: readonly). Он не влияет на выполнение кода, только на проверку типов.

Практические сценарии использования

  • Redux Actions: гарантия, что тип action — это именно литерал, а не строка. Это помогает в discriminated unions (различающихся объединениях).
  • Enum-like объекты: замена enum с более гибкой типизацией.
  • Конфигурации: точные типы для константных конфигов.
  • Type narrowing (сужение типов): помогает TypeScript точно определить, с каким значением вы работаете.

Пример: Redux Action

const ActionTypes = {
  ADD_TODO: 'ADD_TODO',
  REMOVE_TODO: 'REMOVE_TODO',
  TOGGLE_TODO: 'TOGGLE_TODO'
} as const;

type ActionType = typeof ActionTypes[keyof typeof ActionTypes];
// ActionType -> "ADD_TODO" | "REMOVE_TODO" | "TOGGLE_TODO"

// Теперь action creator получает точный тип:
function createAction(type: ActionType, payload: any) {
  return { type, payload } as const;
}

Без as const тип ActionType был бы просто string, что свело бы на нет преимущества TypeScript.

Важное замечание

as const — это только проверка типов. На скомпилированный JavaScript он не влияет. Это инструмент для разработчика, чтобы TypeScript мог точнее проверять ваш код.

<<>>

Альтернативный взгляд: as const как инструмент для discriminated unions

Позвольте дополнить коллегу с другой стороны — как as const помогает в работе с discriminated unions (различающимися объединениями).

Представьте, что у вас есть функция, которая обрабатывает разные типы событий:

type Event = 
  | { type: 'click'; x: number; y: number }
  | { type: 'keypress'; key: string }
  | { type: 'focus'; element: HTMLElement };

function handleEvent(event: Event) {
  switch (event.type) {
    case 'click':
      // TypeScript знает: здесь event.x и event.y доступны
      console.log(event.x, event.y);
      break;
    case 'keypress':
      // TypeScript знает: здесь event.key доступен
      console.log(event.key);
      break;
    case 'focus':
      // TypeScript знает: здесь event.element доступен
      console.log(event.element);
      break;
  }
}

Если бы мы не использовали as const при создании таких объектов, TypeScript не смог бы сузить тип в switch:

const badEvent = { type: 'click', x: 10, y: 20 };
// typeof badEvent.type -> string (не "click")

С as const всё работает идеально:

const goodEvent = { type: 'click', x: 10, y: 20 } as const;
// typeof goodEvent.type -> "click"

Это особенно важно при работе с библиотеками вроде Redux Toolkit, где каждый action creator возвращает объект с type, и для корректной работы discriminated unions нужно, чтобы тип был точным литералом.

Итог: если вы видите, что TypeScript не может определить, с каким именно вариантом объединения вы работаете — попробуйте добавить as const к объекту, который определяет тип.