Back to Blog
TypeScriptconstType ParametersGenerics

TypeScript const Type Parameters

Master TypeScript const type parameters for inferring literal types in generic functions.

B
Bootspring Team
Engineering
January 19, 2019
6 min read

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 exist

Action 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'); // Error

API 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 string

Form 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'));
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 string

vs 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 APIs

Best 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.

Share this article

Help spread the word about Bootspring