infer

Explains infer in CruftScript, which captures part of a type inside a conditional's extends clause and names it for use in the true branch. Covers where it is legal, the exact-constructor structural match, constrained capture with infer U extends V, and why an undecidable match is a hard error.

infer captures part of a type inside a conditional type and gives it a name you can use in the true branch. You write infer NAME inside the extends clause, and it means "match a shape here and remember what filled this slot."

type ElementOf<T> = T extends Array<infer E> ? E : never
type N = ElementOf<Array<number>>   // number
type S = ElementOf<string>          // never (string is not an Array)

This is type-level computation: infer runs at compile time to pull a sub-shape out of a larger type. If you have used generics, think of infer E as a type variable the checker solves for by looking at how the incoming type is built. If you have used TypeScript's infer, the syntax matches; the difference is what happens when the match is unclear.

infer NAME is only legal inside a conditional's extends clause. It has no meaning anywhere else. The name it introduces is in scope only in the conditional's true branch, because that is the only place the capture is known to have succeeded.

type Return<T> = T extends (...args: Args) => infer R ? R : never

How the match works

The checker matches by structural unification: it lines the incoming type up against the pattern and binds each infer name to whatever occupies its position. A parameterized pattern is exact about the constructor. Array<infer U> matches only Array of the same arity; it will not match a different generic that merely happens to hold one type.

type Inner<T> = T extends Array<infer U> ? U : never
type A = Inner<Array<string>>   // string
type B = Inner<Set<string>>     // never: Set is not Array

Constrained capture

You can require the captured type to satisfy a bound by writing infer U extends V. The capture binds only if it is provably assignable to V. If it is not, the pattern does not match and the conditional takes its false branch.

type NumberElement<T> = T extends Array<infer U extends number> ? U : never
type A = NumberElement<Array<number>>   // number (number satisfies the bound)
type B = NumberElement<Array<string>>   // never (string is not assignable to number)

Numeric-literal types (200) do not resolve in the checker today, so the bound and the element types are written with number rather than a literal like Array<200>.

The constraint does real work: it narrows the capture, and it can turn a structural match into a non-match when the captured type falls outside the bound.

When the match cannot be decided

If the checker cannot prove that the pattern matches and cannot prove that it does not, it does not guess. It reports a hard error and leaves an inert placeholder in place of the capture. This is the sharp difference from TypeScript, which would resolve an unmatched infer to unknown and carry on. CruftScript would rather stop than hand you a type that quietly means "we did not actually work this out."

The same rule covers unknown against a structured pattern: an unknown value does not match a shape like Array<infer U>, and it does not widen the capture to make the match succeed. It simply fails to the false branch.

Why it works this way

infer is how you take a type apart, and a tool that takes types apart is only trustworthy if its answers are real. By refusing to invent a capture when the match is undecidable, CruftScript keeps every bound name meaning exactly what it says: the type that genuinely filled that slot. The exact-constructor rule and the constrained form are part of the same discipline, so a pattern matches the shape you wrote and nothing looser.

Limitations

  • Only inside an extends clause. infer has no meaning elsewhere, and its name is visible only in the true branch.
  • Exact constructor and arity. Array<infer U> matches Array of that arity only, not a different generic in the same position.
  • Undecidable match is a hard error. Where TypeScript falls back to unknown, CruftScript stops with a diagnostic and an inert placeholder.