仓库源文站点原文

When working with a fixed set of string constants in TypeScript, there are a few ways to ensure type safety, IDE support, and clean code. This guide explains the differences and trade-offs for const enum and as const.

The Scenario

You have a set of category IDs that are shared across multiple files:

// Category IDs
PRESENTATION: 'cat_123',
POSTER: 'cat_456',
SOCIAL_POST: 'cat_789',

Your goals are:

  1. Type safety
  2. IDE autocomplete
  3. Easy access via Categories.PRESENTATION
  4. Compatibility for use as object keys or in records

Understanding Index Signatures and Mapped Types

Before continuing, it helps to understand how TypeScript models object types, specifically the difference between index signatures and mapped types.

Index Signature: { [key: Type]: Value }

const cache: { [key: string]: number } = {};
cache['item_1'] = 42;

Mapped Type: { [K in Type]: Value }

type CacheKey = 'item_1' | 'item_2';
type Cache = { [K in CacheKey]: number };

const cache: Cache = {
  item_1: 42,
  item_2: 99,
};

The built-in Record<K, T> is just a shorthand for a mapped type using [key in K]. Use Record when you just want a clean key–value mapping. Use [key in K] form when you need more control (optional, readonly, etc.).

Option 1: Object with as const

When you use as const, TypeScript makes the value deeply readonly and narrows all literals to their exact values.

export const Categories = {
  PRESENTATION: 'cat_123',
  POSTER: 'cat_456',
  SOCIAL_POST: 'cat_789',
} as const;

export type Category = (typeof Categories)[keyof typeof Categories];
// Type: 'cat_123' | 'cat_456' | 'cat_789'

Usage Example

// Accessing values
const id = Categories.PRESENTATION; // 'cat_123'

// Works with Record
const styles: Record<Category, string> = {
  [Categories.PRESENTATION]: 'Professional',
  [Categories.POSTER]: 'Creative',
  [Categories.SOCIAL_POST]: 'Casual',
};

Common Pitfall

// This fails
const styles: { [key: Category]: string } = { ... };
// Error: An index signature parameter type cannot be a literal type

You cannot use a literal union type in an index signature. However, Record<Category, string> works because it is implemented as a mapped type, not an index signature.

Option 2: const enum

export const enum Categories {
  PRESENTATION = 'cat_123',
  POSTER = 'cat_456',
  SOCIAL_POST = 'cat_789',
}

Usage Example

// Accessing values
const id = Categories.PRESENTATION; // 'cat_123'

// Works with Record
const styles1: Record<Categories, string> = { ... };

// Works with mapped types
const styles2: { [key in Categories]: string } = { ... };

You cannot use enums in index signatures either:

// Invalid
const styles3: { [key: Categories]: string } = { ... };
// Error: An index signature parameter type cannot be an enum type

Notes

When you use a regular enum, TypeScript generates JavaScript code at runtime to represent it. A real object exists at runtime, and it holds both directions (forward and reverse mappings) for numeric enums, though for string enums only the forward mapping is generated.

A const enum is different: it is completely erased at compile time. No runtime object is generated at all — the compiler inlines the values directly.

export const enum Category {
  PRESENTATION = 'cat_123',
  POSTER = 'cat_456',
}

const id = Category.PRESENTATION;

// Compiles to:
const id = "cat_123";

So for a fixed set of string constants, the practical difference between as const and const enum is minimal. The main distinction isn’t what they can express, but how they behave at runtime and during compilation:

Bonus: Runtime Validation with Zod

Zod adds a third, practical option that bridges runtime and compile-time checks.

If you need runtime validation as well as type safety, libraries like Zod go a step further. With Zod, you can define your constants once, validate them at runtime, and infer their types automatically. It’s heavier than as const or const enum, but ideal when values come from external sources like APIs or configs.