Making constants type-safe in TypeScript
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:
- Type safety
- IDE autocomplete
- Easy access via
Categories.PRESENTATION - 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 }
Typemust bestring,number, orsymbol- Allow any number of keys, all optional
const cache: { [key: string]: number } = {};
cache['item_1'] = 42;
Mapped Type: { [K in Type]: Value }
Typecan be a union or enum- Keys are known and fixed, all required unless marked optional
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]. UseRecordwhen 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
as constcreates a plain JavaScript object at runtime.const enumis erased at compile time (no runtime object) and inlined by the compiler.enum(non-const) generates extra JavaScript code with reverse mappings.- Both
as constandconst enumwork withRecordand mapped types, but not with index signatures. - Modern TypeScript projects often prefer
as constfor 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 constcreates a real JavaScript object. You can inspect, serialize, or iterate over it at runtime.const enumis compile-time only. It disappears after compilation and cannot be referenced dynamically.
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.
