skip to content

In a TypeScript conditional type, what does adding a constraint to an inference variable — as in `infer N extends number` — change compared with a bare `infer N`?

level: middleimportance: nice to knowfreq 20%

answer

  1. extends after the inference name
  2. the candidate has to qualify
  3. failure takes the false branch, not an error
  4. template placeholders normally give strings
  5. numeric literal instead of a string literal

basics

~20 s

A constrained inference variable must satisfy its constraint or the conditional takes the false branch, and in a template-literal placeholder the constraint also makes the compiler infer a literal of that type instead of a string.

solid answer

~40 s

Writing `infer N extends number` attaches a constraint to the candidate. It does two things. First, it filters: if the matched type does not satisfy the constraint, the pattern fails and the conditional takes its false branch, so you get filtering without a nested conditional. Second, inside a template literal placeholder it changes what is inferred — `S extends ${'`'}\${infer N}${'`'}` binds `N` to a string literal such as `"42"`, whereas `S extends ${'`'}\${infer N extends number}${'`'}` binds it to the *numeric* literal `42`. That is the reason the feature exists: without it, digits recovered from a string pattern stayed strings and had to be converted by more type-level machinery. This assumes the modern TypeScript 5.x line; very old compilers reject the syntax outright.

code

typescript · 14 lines
typescript
type ToNumber<S extends string> =
  S extends `${infer N extends number}` ? N : never;

type A = ToNumber<"42">; // 42  (a number literal)
type B = ToNumber<"x">;  // never

type Bare<S extends string> = S extends `${infer N}` ? N : never;
type C = Bare<"42">;     // "42" (a string literal)

type IdOf<T> = T extends { id: infer U extends string } ? U : "no-string-id";
type D = IdOf<{ id: number }>; // "no-string-id"

const n: A = 42;
console.log(n + 1);

go deeper

for a junior

Recognise the infer X extends C spelling when you meet it, and know that the extends there restricts what the pattern will accept rather than declaring a new parameter.

for a middle

Explain both effects precisely: a failing candidate redirects to the false branch, and inside a template literal placeholder the constraint yields a numeric or boolean literal instead of a string literal.

for a senior

Weigh the silent-failure cost — a mismatched constraint produces a fallback type rather than a diagnostic — and choose false branches whose names make the failure legible to whoever hits it.

for a principal

Own the compiler-version floor this syntax implies for a published package, and decide whether string-encoded type information should exist in the design at all.

## Two jobs in one piece of syntax A bare `infer N` accepts whatever occupies the slot. Adding `extends` after the name — `infer N extends number` — constrains the candidate, and the compiler uses that constraint in two distinct ways. ## Job one: filtering into the false branch ```ts type IdOf<T> = T extends { id: infer U extends string } ? U : "no-string-id"; type A = IdOf<{ id: "abc" }>; // "abc" type B = IdOf<{ id: number }>; // "no-string-id" ``` When the candidate fails the constraint the whole pattern fails, so the conditional falls to the false branch. Note what does *not* happen: it is not a compile error, and the variable is not silently widened or narrowed to the constraint. It is a match failure, exactly like passing a non-array to an array pattern. The alternative spelling is a nested conditional — infer without a constraint, then test the result. The constrained form is shorter and keeps the intent in one place, which matters when a pattern already has several inference sites. ## Job two: changing what is inferred in a template literal This is the reason the feature was added. Placeholders in template literal types normally yield string literals, because that is what a string is made of: ```ts type Bare<S extends string> = S extends `${infer N}` ? N : never; type C = Bare<"42">; // "42" — a string literal ``` With a numeric constraint the compiler produces a numeric literal instead: ```ts type ToNumber<S extends string> = S extends `${infer N extends number}` ? N : never; type D = ToNumber<"42">; // 42 — a number literal type E = ToNumber<"x">; // never — the constraint failed ``` That single change turns a common chore — recovering a numeric value that arrived encoded in a string type — into one line. `infer B extends boolean` behaves the same way for `"true"` and `"false"`, and `infer K extends string` is used when you want to constrain a captured key to something usable as a property name. ## What it is not It is easy to conflate this with two other constraint mechanisms. A constraint on a *type parameter* — `type F<T extends string> = ...` — restricts what callers may pass and produces an error at the use site when violated. A constraint on an *inference variable* restricts what the pattern will match and produces a silent branch change instead. Same keyword, different consequence, and mixing them up leads people to expect an error where the code simply takes the other branch. It is also not a conversion. `infer N extends number` does not turn a string into a number at runtime, because there is no runtime involved at all: the conditional type is erased, and the numeric literal exists only in the checker's model of the program. If you also need the value at runtime you still have to parse the string in JavaScript; the type-level and value-level work are independent. ## Practical guidance Use the constrained form when a helper only makes sense for a particular kind of candidate, and let the false branch express the fallback — a sentinel literal reads far better in an error message than a bare `never`. Reach for it especially when pulling structured data out of string-shaped types, where recovering a numeric or boolean literal directly saves an entire layer of conversion machinery. This discussion assumes the TypeScript 5.x line and later. The constrained form is not universally available in very old compilers, so it is worth confirming the compiler version before using it in a package that supports a wide range of consumers.

  • What happens when the candidate fails the constraint — an error, a widened type, or something else?
    Something else: the pattern simply does not match, so the conditional evaluates its false branch. There is no error and no widening. That makes the constrained form a compact filter, but it also means a mistyped constraint fails silently — you get the fallback type rather than a diagnostic pointing at the cause.
  • How is a constraint on an inference variable different from a constraint on the type alias's own type parameter?
    A type-parameter constraint restricts what callers may pass and errors at the use site when violated. An inference-variable constraint restricts what the pattern matches and quietly redirects to the false branch. The first is a contract with the caller; the second is part of the matching logic inside the conditional.
  • Does `infer N extends number` convert anything at runtime?
    No. Conditional types and inference variables are erased at compile time, so the numeric literal exists only in the checker's model. If the program also needs the number at runtime you still parse the string in JavaScript; the type-level result and the runtime value are produced by entirely separate work.

saying these in an interview costs you the question

  • Thinks the constraint widens the inferred type to the constraint
  • Expects a failed constraint to be a compile error
  • Believes a bare template placeholder can yield a number
  • Confuses it with a constraint on the alias's type parameter
  • Claims the constrained form converts a string at runtime

context