Back to Blog
TypeScriptconstLiteral TypesImmutability

TypeScript const Assertions Guide

Master TypeScript const assertions for literal types, immutable objects, and readonly arrays.

B
Bootspring Team
Engineering
December 21, 2019
7 min read

The as const assertion creates the narrowest possible type from a value. Here's how to use it.

Basic Usage#

1// Without as const - widened types 2let color = 'red'; // string 3let count = 42; // number 4let active = true; // boolean 5 6// With as const - literal types 7let color2 = 'red' as const; // 'red' 8let count2 = 42 as const; // 42 9let active2 = true as const; // true 10 11// Object without as const 12const config = { 13 api: 'https://api.example.com', 14 timeout: 5000, 15}; 16// { api: string; timeout: number } 17 18// Object with as const 19const config2 = { 20 api: 'https://api.example.com', 21 timeout: 5000, 22} as const; 23// { readonly api: 'https://api.example.com'; readonly timeout: 5000 }

Arrays and Tuples#

1// Array without as const 2const colors = ['red', 'green', 'blue']; 3// string[] 4 5// Array with as const - becomes readonly tuple 6const colors2 = ['red', 'green', 'blue'] as const; 7// readonly ['red', 'green', 'blue'] 8 9// Access specific element type 10type FirstColor = typeof colors2[0]; // 'red' 11type AllColors = typeof colors2[number]; // 'red' | 'green' | 'blue' 12 13// Tuple with as const 14const point = [10, 20] as const; 15// readonly [10, 20] 16 17// Mixed array 18const mixed = [1, 'hello', true] as const; 19// readonly [1, 'hello', true] 20 21// Function arguments 22function setPosition(x: number, y: number) { 23 console.log(x, y); 24} 25 26const coords = [10, 20] as const; 27setPosition(...coords); // Works - TypeScript knows it's [number, number]

Enum-like Constants#

1// Object as enum 2const Direction = { 3 Up: 'UP', 4 Down: 'DOWN', 5 Left: 'LEFT', 6 Right: 'RIGHT', 7} as const; 8 9type Direction = typeof Direction[keyof typeof Direction]; 10// 'UP' | 'DOWN' | 'LEFT' | 'RIGHT' 11 12function move(direction: Direction) { 13 console.log(`Moving ${direction}`); 14} 15 16move(Direction.Up); // OK 17move('UP'); // OK 18move('DIAGONAL'); // Error 19 20// Numeric enum-like 21const HttpStatus = { 22 OK: 200, 23 Created: 201, 24 BadRequest: 400, 25 NotFound: 404, 26 ServerError: 500, 27} as const; 28 29type HttpStatus = typeof HttpStatus[keyof typeof HttpStatus]; 30// 200 | 201 | 400 | 404 | 500 31 32// With descriptions 33const ErrorCode = { 34 NotFound: { code: 404, message: 'Not Found' }, 35 Unauthorized: { code: 401, message: 'Unauthorized' }, 36 ServerError: { code: 500, message: 'Server Error' }, 37} as const; 38 39type ErrorCodeKey = keyof typeof ErrorCode; 40// 'NotFound' | 'Unauthorized' | 'ServerError'

Immutable Objects#

1// Deep readonly with as const 2const theme = { 3 colors: { 4 primary: '#007bff', 5 secondary: '#6c757d', 6 success: '#28a745', 7 }, 8 spacing: { 9 small: 8, 10 medium: 16, 11 large: 24, 12 }, 13 breakpoints: { 14 mobile: 480, 15 tablet: 768, 16 desktop: 1024, 17 }, 18} as const; 19 20// All nested properties are readonly 21// theme.colors.primary = '#000'; // Error: Cannot assign to readonly property 22 23// Type extraction 24type Theme = typeof theme; 25type Colors = typeof theme.colors; 26type ColorValue = typeof theme.colors[keyof typeof theme.colors]; 27// '#007bff' | '#6c757d' | '#28a745' 28 29// Accessing values 30function getPrimaryColor(): typeof theme.colors.primary { 31 return theme.colors.primary; 32}

Function Parameters#

1// Literal type parameters 2function createAction<T extends string>(type: T) { 3 return { type }; 4} 5 6// Without as const 7const action1 = createAction('INCREMENT'); 8// { type: string } 9 10// With as const 11const action2 = createAction('INCREMENT' as const); 12// { type: 'INCREMENT' } 13 14// Or using const type parameter (TS 5.0+) 15function createAction2<const T extends string>(type: T) { 16 return { type }; 17} 18 19const action3 = createAction2('INCREMENT'); 20// { type: 'INCREMENT' } - automatically inferred as literal 21 22// Options object 23function configure<const T extends { mode: string; debug: boolean }>( 24 options: T 25) { 26 return options; 27} 28 29const opts = configure({ mode: 'production', debug: false }); 30// { mode: 'production'; debug: false }

Route Definitions#

1// API routes 2const routes = { 3 home: '/', 4 users: '/users', 5 userDetail: '/users/:id', 6 posts: '/posts', 7 postDetail: '/posts/:id', 8} as const; 9 10type Route = typeof routes[keyof typeof routes]; 11// '/' | '/users' | '/users/:id' | '/posts' | '/posts/:id' 12 13function navigate(route: Route) { 14 // Type-safe navigation 15} 16 17navigate(routes.home); // OK 18navigate('/users'); // OK 19navigate('/invalid'); // Error 20 21// Route config with methods 22const apiRoutes = { 23 getUsers: { path: '/users', method: 'GET' }, 24 createUser: { path: '/users', method: 'POST' }, 25 getUser: { path: '/users/:id', method: 'GET' }, 26 updateUser: { path: '/users/:id', method: 'PUT' }, 27 deleteUser: { path: '/users/:id', method: 'DELETE' }, 28} as const; 29 30type ApiRoute = typeof apiRoutes[keyof typeof apiRoutes]; 31// { readonly path: '/users'; readonly method: 'GET' } | ...

Action Types (Redux-like)#

1// Action type constants 2const ActionTypes = { 3 ADD_TODO: 'todos/ADD_TODO', 4 REMOVE_TODO: 'todos/REMOVE_TODO', 5 TOGGLE_TODO: 'todos/TOGGLE_TODO', 6 SET_FILTER: 'filter/SET_FILTER', 7} as const; 8 9// Action creators with literal types 10function addTodo(text: string) { 11 return { 12 type: ActionTypes.ADD_TODO, 13 payload: { text }, 14 } as const; 15} 16 17function removeTodo(id: number) { 18 return { 19 type: ActionTypes.REMOVE_TODO, 20 payload: { id }, 21 } as const; 22} 23 24// Union of all actions 25type Action = ReturnType<typeof addTodo> | ReturnType<typeof removeTodo>; 26 27// Reducer with discriminated union 28function reducer(state: State, action: Action) { 29 switch (action.type) { 30 case ActionTypes.ADD_TODO: 31 // action.payload is { text: string } 32 return { ...state, todos: [...state.todos, action.payload.text] }; 33 case ActionTypes.REMOVE_TODO: 34 // action.payload is { id: number } 35 return { 36 ...state, 37 todos: state.todos.filter((_, i) => i !== action.payload.id), 38 }; 39 } 40}

Validation Schemas#

1// Form field types 2const FieldTypes = { 3 text: 'text', 4 email: 'email', 5 password: 'password', 6 number: 'number', 7 select: 'select', 8} as const; 9 10type FieldType = typeof FieldTypes[keyof typeof FieldTypes]; 11 12// Schema definition 13const userSchema = { 14 name: { type: 'text', required: true }, 15 email: { type: 'email', required: true }, 16 age: { type: 'number', required: false }, 17 role: { type: 'select', options: ['admin', 'user', 'guest'] }, 18} as const; 19 20// Extract field names 21type UserFields = keyof typeof userSchema; 22// 'name' | 'email' | 'age' | 'role' 23 24// Extract role options 25type RoleOption = typeof userSchema.role.options[number]; 26// 'admin' | 'user' | 'guest'

Event Maps#

1// Event name to payload mapping 2const Events = { 3 USER_LOGIN: 'user:login', 4 USER_LOGOUT: 'user:logout', 5 ITEM_ADDED: 'cart:item_added', 6 ITEM_REMOVED: 'cart:item_removed', 7} as const; 8 9type EventName = typeof Events[keyof typeof Events]; 10 11interface EventPayloads { 12 'user:login': { userId: string; timestamp: Date }; 13 'user:logout': { userId: string }; 14 'cart:item_added': { itemId: string; quantity: number }; 15 'cart:item_removed': { itemId: string }; 16} 17 18function emit<E extends EventName>( 19 event: E, 20 payload: EventPayloads[E] 21) { 22 // Type-safe event emission 23} 24 25emit(Events.USER_LOGIN, { 26 userId: '123', 27 timestamp: new Date(), 28});

Combining with satisfies#

1// Validate type while preserving literals 2type Config = { 3 env: string; 4 port: number; 5 debug: boolean; 6}; 7 8const config = { 9 env: 'production', 10 port: 3000, 11 debug: false, 12} as const satisfies Config; 13 14// config.env is 'production', not string 15// config.port is 3000, not number 16// But it's validated against Config type 17 18// Error if invalid 19const badConfig = { 20 env: 'production', 21 port: '3000', // Error: Type 'string' not assignable to 'number' 22 debug: false, 23} as const satisfies Config;

Best Practices#

Usage: ✓ Use for enum-like constants ✓ Use for configuration objects ✓ Use for route definitions ✓ Use for action types Benefits: ✓ Narrowest possible types ✓ Deep readonly ✓ Literal type inference ✓ Better autocomplete Patterns: ✓ Combine with typeof ✓ Extract union types ✓ Use with satisfies ✓ Create type-safe enums Avoid: ✗ On mutable data ✗ When you need assignment ✗ For temporary values ✗ Overusing everywhere

Conclusion#

The as const assertion creates the narrowest literal types and makes values deeply readonly. Use it for configuration, constants, enum alternatives, and action types. Combine with typeof and keyof to extract useful types, and use satisfies for validation while preserving literal types.

Share this article

Help spread the word about Bootspring