🚀AdvancedType System Mastery
`const` Type Parameters & NoInfer
Use TypeScript 5.0's const type parameters to preserve literal types in generic functions, and NoInfer to control where TypeScript deduces types.
Overview
TypeScript 5.0 introduced two features that make generic inference much more precise.
**`const` type parameters** let a generic function infer the most specific (narrowest) type possible, as if the caller had written `as const`. Without it, TypeScript widens array literals to `string[]` and string literals to `string`. With `const T`, you get the literal tuple `["a", "b", "c"]` instead of `string[]`.
**`NoInfer<T>`** (TypeScript 5.4) prevents TypeScript from using a type parameter position when deciding what to infer. If you have `function set<T>(values: T[], default: NoInfer<T>)`, TypeScript infers T only from the first argument - not from the second, preventing the second from "widening" T.
Together these give library authors precise control over type inference, enabling APIs that feel magical: pass a plain array literal and get fully typed narrowed results back without any `as const` at the call site.
Key Points
- •Without `const`: `function f<T>(x: T[])` infers `string[]` from `["a", "b"]`
- •With `const`: `function f<const T>(x: T[])` infers `readonly ["a", "b"]` from `["a", "b"]`
- •`const T` applies recursive narrowing: nested objects and arrays also get literal types
- •`NoInfer<T>` blocks inference from that position: T is inferred from other arguments only
- •Use `const T` for: route definitions, event name arrays, config objects, schema builders
- •Use `NoInfer<T>` for: default values, fallbacks, secondary arguments that should not drive T
- •Combines well with template literal types for fully inferred string-based APIs
Examples
const type parameter - preserving literal types
// Without const: inference widens to string[]
function createRoute<T extends string>(paths: T[]): T[] {
return paths;
}
const r1 = createRoute(['home', 'about', 'contact']);
// T is string (widened!) - r1 is string[], not ["home", "about", "contact"]
// With const: inference preserves literal tuple type
function createRouteConst<const T extends string>(paths: T[]): T[] {
return paths;
}
const r2 = createRouteConst(['home', 'about', 'contact']);
// T is "home" | "about" | "contact"
// r2 type is ("home" | "about" | "contact")[]
// Practical: type-safe event system
function createEventSystem<const Events extends string>(events: Events[]) {
const handlers = new Map<Events, Set<() => void>>();
return {
on(event: Events, handler: () => void): void {
if (!handlers.has(event)) handlers.set(event, new Set());
handlers.get(event)!.add(handler);
},
emit(event: Events): void {
handlers.get(event)?.forEach(h => h());
},
events,
};
}
const bus = createEventSystem(['login', 'logout', 'purchase']);
bus.on('login', () => console.log('User logged in'));
bus.emit('login'); // OK - TypeScript knows "login" is valid
// bus.emit('signup'); // Error - "signup" not in the listNoInfer - controlling inference sites
// Problem: T is inferred from BOTH arguments, which widens it
function createWithDefault_broken<T>(values: T[], defaultValue: T): T {
return values[0] ?? defaultValue;
}
// Caller: T inferred as string | null because of null default
const r1 = createWithDefault_broken(['a', 'b'], null);
// T is string | null - values could contain null now!
// Fix: NoInfer prevents defaultValue from contributing to T
function createWithDefault<T>(values: T[], defaultValue: NoInfer<T>): T {
return values[0] ?? defaultValue;
}
const r2 = createWithDefault(['a', 'b'], null);
// Now: Error! null is not assignable to NoInfer<string>
// T is inferred as string from first arg; null fails to match
const r3 = createWithDefault(['a', 'b'], 'fallback');
// T is string; 'fallback' satisfies NoInfer<string> - OK
// Practical: generic list with typed selection
function listWithDefault<const T extends string>(
options: T[],
selected: NoInfer<T> = options[0] as NoInfer<T>
): { options: T[]; selected: T } {
return { options, selected };
}
const colorPicker = listWithDefault(['red', 'green', 'blue'], 'red');
// options: ("red" | "green" | "blue")[]
// selected: "red" | "green" | "blue"Combining const T with template literals for typed builders
// A route builder where all route names are inferred
function defineRoutes<const Routes extends Record<string, string>>(
routes: Routes
): Routes & { resolve(name: keyof Routes): string } {
return {
...routes,
resolve(name: keyof Routes): string {
return routes[name] as string;
},
};
}
const ROUTES = defineRoutes({
home: '/',
users: '/users',
profile: '/users/:id',
posts: '/users/:id/posts',
});
// Fully typed - TypeScript knows all route names
const path = ROUTES.resolve('users'); // OK
// ROUTES.resolve('unknown'); // Error - not a key
// Column selector using const T
function select<const T extends string, Row extends Record<T, unknown>>(
rows: Row[],
columns: T[]
): Pick<Row, T>[] {
return rows.map(row => {
const result = {} as Pick<Row, T>;
for (const col of columns) result[col] = row[col];
return result;
});
}
const data = [
{ id: 1, name: 'Alice', email: 'a@a.com', age: 30 },
{ id: 2, name: 'Bob', email: 'b@b.com', age: 25 },
];
const names = select(data, ['id', 'name']);
// names: { id: number; name: string }[] - fully typed!Common Mistakes
- Adding `const` to every generic function: only use it when you need literal type preservation. Most generic functions work fine with widened inference and adding `const` can make types awkwardly narrow at call sites
- Confusing `const T` (type parameter modifier) with `as const` (value assertion): `const T` affects how TypeScript infers T at call sites; `as const` affects what type a value expression has
- Using `NoInfer` where a constraint would be cleaner: `NoInfer` is for when you want T inferred from one argument but validated (not inferred from) another; if you just want a constraint, `extends T` on the parameter is clearer
Note
The best use cases for `const T` are APIs that take arrays or object literals and give back typed results based on their exact content: event systems, route definitions, column selectors, and schema builders. If the caller always passes a variable (not a literal), `const T` provides no benefit.
Exercises
Write TypeScript code to solve each exercise. Click Run to compile and execute it right here, then mark as done.
Loading editor…
Completed: 0 / 2 exercises
Knowledge Check
Question 1 of 4