You used to need as const everywhere. On your config objects. On your route definitions. On your action creators. On anything where you wanted TypeScript to infer the narrowest possible type instead of widening to string or number.
TypeScript 7 makes the compiler smart enough to figure it out. Const type parameters let you declare that a generic type parameter should be inferred as narrowly as possible — no as const required at the call site.
This is a small feature with an outsized impact. It eliminates boilerplate, reduces errors, and makes generic APIs feel native.
The Problem: Type Widening
TypeScript widens types by default:
const name = "Alice"; // Type: "Alice" (const — can't change)
let name2 = "Alice"; // Type: string (let — could change)
const config = {
url: "https://api.example.com",
method: "GET",
};
// Type: { url: string; method: string } — widened!
The config object's properties are widened to string even though it's declared with const. Why? Because objects declared with const can still have their properties mutated. TypeScript assumes you might change config.method to "POST" later.
The fix used to be as const:
const config = {
url: "https://api.example.com",
method: "GET",
} as const;
// Type: { readonly url: "https://api.example.com"; readonly method: "GET" }
as const tells TypeScript: "Treat this as immutable. Infer the narrowest possible types. Make everything readonly."
This works, but it's verbose. And it's easy to forget. And when you forget, your types are wider than you intended, and bugs slip through.
The Solution: Const Type Parameters
TypeScript 7 introduces const type parameters. Add const before a type parameter, and TypeScript infers the narrowest possible type:
function createConfig<const T>(config: T): T {
return config;
}
const config = createConfig({
url: "https://api.example.com",
method: "GET",
});
// Type: { readonly url: "https://api.example.com"; readonly method: "GET" }
// No 'as const' needed!
The const modifier on <const T> tells TypeScript: "When inferring T from the argument, use the narrowest possible type — as if the caller had written as const."
Before and After
// Before TypeScript 7
function useState<const T>(initial: T): [T, (value: T) => void] {
// ...
}
const [count, setCount] = useState(0);
// count: 0 (literal), not number!
// Before: you'd need
const [count, setCount] = useState(0 as const);
// or
const [count, setCount] = useState<0>(0);
// TypeScript 7
function useState<const T>(initial: T): [T, (value: T) => void] {
// ...
}
const [count, setCount] = useState(0);
// count: 0 — automatically!
How Const Type Parameters Work
When you write <const T>, TypeScript applies these rules when inferring T:
- String literals stay literal.
"hello"is inferred as"hello", notstring. - Number literals stay literal.
42is inferred as42, notnumber. - Boolean literals stay literal.
trueis inferred astrue, notboolean. - Arrays become readonly tuples.
[1, 2, 3]is inferred asreadonly [1, 2, 3], notnumber[]. - Object properties become readonly.
{ x: 10 }is inferred as{ readonly x: 10 }, not{ x: number }.
Const Type Parameters with Arrays
function first<const T extends readonly unknown[]>(arr: T): T[0] {
return arr[0];
}
const result = first([1, 2, 3]);
// result: 1 (literal), not number!
Const Type Parameters with Objects
function createStore<const T>(initial: T) {
let state = initial;
return {
get: (): T => state,
set: (newState: T): void => { state = newState; },
};
}
const store = createStore({ name: "Alice", age: 30 });
// store.get().name: "Alice" (literal), not string!
Real-World Use Cases
Use Case 1: Type-Safe Routing
function defineRoutes<const T extends Record<string, string>>(routes: T): T {
return routes;
}
const routes = defineRoutes({
home: "/",
about: "/about",
user: "/user/:id",
});
// routes.home: "/" (literal)
// routes.about: "/about" (literal)
// routes.user: "/user/:id" (literal)
Use Case 2: Action Creators
function createAction<const T extends string, const P>(type: T, payload: P) {
return { type, payload };
}
const addTodo = createAction("ADD_TODO", { text: "Buy milk" });
// addTodo.type: "ADD_TODO" (literal)
// addTodo.payload: { readonly text: "Buy milk" }
Use Case 3: Configuration Objects
function defineConfig<const T>(config: T): T {
return config;
}
const dbConfig = defineConfig({
host: "localhost",
port: 5432,
database: "myapp",
pool: {
min: 2,
max: 10,
},
});
// All properties are literal types, deeply readonly
Use Case 4: Enum-Like Objects
function createEnum<const T extends Record<string, string | number>>(values: T): T {
return values;
}
const Status = createEnum({
IDLE: "idle",
LOADING: "loading",
SUCCESS: "success",
ERROR: "error",
});
// Status.IDLE: "idle" (literal)
// Status.LOADING: "loading" (literal)
Const Type Parameters vs. as const
| Feature | as const | const type parameter |
|---|---|---|
| Where to write | At every call site | Once, in the function definition |
| Forgetting | Easy to forget | Can't forget (enforced by the function) |
| Readonly | Makes everything readonly | Same behavior |
| Inference | Narrows types | Same behavior |
| Flexibility | Caller decides | Function author decides |
The key advantage: const type parameters move the decision from the call site to the definition. The function author decides "this should always be inferred narrowly." Callers get the right types automatically.
When NOT to Use Const Type Parameters
Const type parameters aren't always the right choice:
// DON'T use const when you WANT widening
function identity<T>(value: T): T {
return value;
}
const x = identity("hello"); // string (widened) — usually what you want
const y = identity(42); // number (widened) — usually what you want
// If you used <const T>, x would be "hello" and y would be 42
// That's too narrow for most identity-function use cases
Use const type parameters when:
- The function is designed to work with literal types
- The return type should preserve the exact input types
- Callers would otherwise need
as const
Don't use const type parameters when:
- The function should work with widened types
- The function transforms values in ways that make literal types meaningless
- The function is a general-purpose utility where widening is expected
Combining Const Type Parameters with Other Features
With Generic Constraints
function pick<const T extends Record<string, unknown>, const K extends keyof T>(
obj: T,
keys: K[]
): Pick<T, K> {
const result = {} as Pick<T, K>;
for (const key of keys) {
result[key] = obj[key];
}
return result;
}
const user = { name: "Alice", age: 30, email: "alice@example.com" };
const picked = pick(user, ["name", "email"]);
// picked.name: "Alice" (literal)
// picked.email: "alice@example.com" (literal)
With Template Literal Types
function defineEvent<const T extends string>(name: T) {
return {
event: name,
handler: `on${Capitalize<T>}` as const,
};
}
const click = defineEvent("click");
// click.event: "click"
// click.handler: "onClick"
Try This: Const Type Parameters
- Write a
createConfigfunction with a const type parameter. Pass it a config object and observe the inferred types. - Write a
defineRoutesfunction that preserves literal URL types. - Write a
createActionfunction that preserves literal action types and payload shapes. - Compare the inferred types with and without
conston the type parameter. - Write a
tuplefunction that takes variadic arguments and returns them as a readonly tuple with literal types.
Time needed: 20 minutes.
What to notice: How const type parameters eliminate as const at call sites. How the types are deeply readonly and literal. How the function author controls inference behavior, not the caller.
The Question
You now know about const type parameters — a small feature that eliminates a common pain point. But TypeScript 7 has more: using declarations for resource management, improved ESM support, type-only imports 2.0, and significant compiler speed improvements.
The next chapter is a tour of everything else that's new in TypeScript 7. The features you'll use every day, and the ones you'll appreciate when you need them.
In the next chapter: the complete TypeScript 7 changelog — using declarations, ESM improvements, type-only imports 2.0, compiler performance, and everything else that makes TypeScript 7 the best version yet.