The const modifier on type parameters infers literal types instead of widening to general types. Here's how to use it.
Basic Concept#
1// Without const - types widen
2function withoutConst<T>(value: T): T {
3 return value;
4}
5
6const result1 = withoutConst(['a', 'b', 'c']);
7// result1: string[]
8
9// With const - literal types preserved
10function withConst<const T>(value: T): T {
11 return value;
12}
13
14const result2 = withConst(['a', 'b', 'c']);
15// result2: readonly ['a', 'b', 'c']Object Literals#
1// Without const
2function createConfig<T>(config: T): T {
3 return config;
4}
5
6const config1 = createConfig({
7 endpoint: '/api',
8 method: 'GET',
9});
10// config1: { endpoint: string; method: string }
11
12// With const
13function createConstConfig<const T>(config: T): T {
14 return config;
15}
16
17const config2 = createConstConfig({
18 endpoint: '/api',
19 method: 'GET',
20});
21// config2: { readonly endpoint: '/api'; readonly method: 'GET' }Route Definitions#
1// Type-safe route definitions
2function defineRoutes<const T extends Record<string, string>>(routes: T): T {
3 return routes;
4}
5
6const routes = defineRoutes({
7 home: '/',
8 about: '/about',
9 users: '/users/:id',
10 posts: '/posts/:postId/comments/:commentId',
11});
12
13// routes.home is '/', not string
14// routes.about is '/about', not string
15
16type RouteKeys = keyof typeof routes;
17// 'home' | 'about' | 'users' | 'posts'
18
19// Extract route params
20type ExtractParams<T extends string> =
21 T extends `${string}:${infer Param}/${infer Rest}`
22 ? Param | ExtractParams<Rest>
23 : T extends `${string}:${infer Param}`
24 ? Param
25 : never;
26
27type PostsParams = ExtractParams<typeof routes.posts>;
28// 'postId' | 'commentId'Event Handlers#
1function createEventHandlers<
2 const T extends Record<string, (...args: any[]) => void>
3>(handlers: T): T {
4 return handlers;
5}
6
7const handlers = createEventHandlers({
8 onClick: (x: number, y: number) => console.log(x, y),
9 onHover: (element: HTMLElement) => console.log(element),
10 onSubmit: (data: FormData) => console.log(data),
11});
12
13// Type-safe handler access
14handlers.onClick(10, 20);
15// handlers.onUnknown(); // Error: Property doesn't existAction Creators#
1// Redux-style action creators
2function createAction<const T extends string>(type: T) {
3 return <P>(payload: P) => ({
4 type,
5 payload,
6 } as const);
7}
8
9const increment = createAction('INCREMENT');
10const decrement = createAction('DECREMENT');
11
12const action1 = increment(5);
13// { type: 'INCREMENT'; payload: number }
14
15const action2 = decrement(1);
16// { type: 'DECREMENT'; payload: number }
17
18// Action types are literal strings
19type IncrementAction = ReturnType<ReturnType<typeof increment>>;
20// { readonly type: 'INCREMENT'; readonly payload: number }Tuple Inference#
1// Without const - array type
2function tuple<T extends unknown[]>(...args: T): T {
3 return args;
4}
5
6const t1 = tuple(1, 'hello', true);
7// t1: [number, string, boolean]
8
9// With const - literal tuple
10function constTuple<const T extends unknown[]>(...args: T): T {
11 return args;
12}
13
14const t2 = constTuple(1, 'hello', true);
15// t2: readonly [1, 'hello', true]
16
17// Useful for fixed configurations
18const dimensions = constTuple(1920, 1080);
19// dimensions: readonly [1920, 1080]State Machine#
1function createStateMachine<
2 const States extends string,
3 const Transitions extends Record<States, readonly States[]>
4>(config: { states: readonly States[]; transitions: Transitions }) {
5 return {
6 canTransition(from: States, to: States): boolean {
7 return config.transitions[from]?.includes(to) ?? false;
8 },
9 getNextStates(current: States): Transitions[States] {
10 return config.transitions[current];
11 },
12 };
13}
14
15const machine = createStateMachine({
16 states: ['idle', 'loading', 'success', 'error'] as const,
17 transitions: {
18 idle: ['loading'],
19 loading: ['success', 'error'],
20 success: ['idle'],
21 error: ['idle', 'loading'],
22 } as const,
23});
24
25// Type-safe state transitions
26machine.canTransition('idle', 'loading'); // OK
27// machine.canTransition('idle', 'unknown'); // ErrorAPI Endpoints#
1function defineAPI<
2 const T extends Record<
3 string,
4 { method: 'GET' | 'POST' | 'PUT' | 'DELETE'; path: string }
5 >
6>(endpoints: T): T {
7 return endpoints;
8}
9
10const api = defineAPI({
11 getUsers: { method: 'GET', path: '/users' },
12 getUser: { method: 'GET', path: '/users/:id' },
13 createUser: { method: 'POST', path: '/users' },
14 updateUser: { method: 'PUT', path: '/users/:id' },
15 deleteUser: { method: 'DELETE', path: '/users/:id' },
16});
17
18// api.getUsers.method is 'GET', not 'GET' | 'POST' | 'PUT' | 'DELETE'
19// api.getUsers.path is '/users', not stringForm Fields#
1function defineForm<
2 const T extends Record<string, { type: string; required?: boolean }>
3>(fields: T): T {
4 return fields;
5}
6
7const loginForm = defineForm({
8 email: { type: 'email', required: true },
9 password: { type: 'password', required: true },
10 rememberMe: { type: 'checkbox', required: false },
11});
12
13// Type-safe field access
14type FieldNames = keyof typeof loginForm;
15// 'email' | 'password' | 'rememberMe'
16
17// Extract required fields
18type RequiredFields = {
19 [K in keyof typeof loginForm]: typeof loginForm[K]['required'] extends true
20 ? K
21 : never;
22}[keyof typeof loginForm];
23// 'email' | 'password'Validation Rules#
1function createValidator<
2 const T extends Record<string, readonly ((value: any) => boolean)[]>
3>(rules: T): T {
4 return rules;
5}
6
7const validators = createValidator({
8 username: [
9 (v: string) => v.length >= 3,
10 (v: string) => /^[a-z0-9]+$/.test(v),
11 ],
12 email: [(v: string) => v.includes('@')],
13 age: [(v: number) => v >= 18],
14} as const);
15
16// Type-safe validator access
17validators.username.forEach((rule) => rule('test'));Menu Configuration#
1function createMenu<
2 const T extends readonly {
3 id: string;
4 label: string;
5 children?: readonly { id: string; label: string }[];
6 }[]
7>(items: T): T {
8 return items;
9}
10
11const menu = createMenu([
12 { id: 'home', label: 'Home' },
13 {
14 id: 'products',
15 label: 'Products',
16 children: [
17 { id: 'software', label: 'Software' },
18 { id: 'hardware', label: 'Hardware' },
19 ],
20 },
21 { id: 'about', label: 'About' },
22] as const);
23
24// menu[0].id is 'home', not string
25// menu[1].children[0].id is 'software', not stringvs as const#
1// as const on the value
2const config1 = {
3 api: '/api',
4 timeout: 5000,
5} as const;
6// { readonly api: '/api'; readonly timeout: 5000 }
7
8// const type parameter in function
9function createConfig<const T>(config: T): T {
10 return config;
11}
12
13const config2 = createConfig({
14 api: '/api',
15 timeout: 5000,
16});
17// { readonly api: '/api'; readonly timeout: 5000 }
18
19// const type parameter is useful when:
20// 1. You're creating a utility function
21// 2. You want users to get literal types automatically
22// 3. You're building type-safe APIsBest Practices#
When to Use:
✓ Route/path definitions
✓ Configuration objects
✓ Action types/creators
✓ State machine definitions
✓ API endpoint definitions
Benefits:
✓ Literal types preserved
✓ Better autocomplete
✓ Type-safe key access
✓ Enables type extraction
Patterns:
✓ Combine with as const for arrays
✓ Use for builder patterns
✓ Define schemas with literals
✓ Create type-safe enums
Avoid:
✗ When widening is desired
✗ For user-provided data
✗ When literals aren't useful
✗ Overusing for simple types
Conclusion#
The const modifier on type parameters preserves literal types instead of widening them. Use it for configuration objects, route definitions, action creators, and any scenario where you want the specific literal types rather than general types like string or number. It's particularly powerful when combined with template literal types and conditional types to extract information from the literal values at the type level.