Skip to main content

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:

  1. String literals stay literal. "hello" is inferred as "hello", not string.
  2. Number literals stay literal. 42 is inferred as 42, not number.
  3. Boolean literals stay literal. true is inferred as true, not boolean.
  4. Arrays become readonly tuples. [1, 2, 3] is inferred as readonly [1, 2, 3], not number[].
  5. 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​

Featureas constconst type parameter
Where to writeAt every call siteOnce, in the function definition
ForgettingEasy to forgetCan't forget (enforced by the function)
ReadonlyMakes everything readonlySame behavior
InferenceNarrows typesSame behavior
FlexibilityCaller decidesFunction 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​

  1. Write a createConfig function with a const type parameter. Pass it a config object and observe the inferred types.
  2. Write a defineRoutes function that preserves literal URL types.
  3. Write a createAction function that preserves literal action types and payload shapes.
  4. Compare the inferred types with and without const on the type parameter.
  5. Write a tuple function 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.