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.
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:
Categories.PRESENTATIONBefore continuing, it helps to understand how TypeScript models object types, specifically the difference between index signatures and mapped types.
{ [key: Type]: Value }Type must be string, number, or symbolconst cache: { [key: string]: number } = {};
cache['item_1'] = 42;
{ [K in Type]: Value }Type can be a union or enumtype 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]. UseRecordwhen you just want a clean key–value mapping. Use[key in K]form when you need more control (optional, readonly, etc.).
as constWhen 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'
// 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',
};
// 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.
const enumexport const enum Categories {
PRESENTATION = 'cat_123',
POSTER = 'cat_456',
SOCIAL_POST = 'cat_789',
}
// 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
as const creates a plain JavaScript object at runtime.const enum is erased at compile time (no runtime object) and inlined by the compiler.enum (non-const) generates extra JavaScript code with reverse mappings.as const and const enum work with Record and mapped types, but not with index signatures.as const for simplicity and full runtime visibility.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 enumis 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:
as const creates a real JavaScript object. You can inspect, serialize, or iterate over it at runtime.const enum is compile-time only. It disappears after compilation and cannot be referenced dynamically.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.