Type inference
Explains how CruftScript infers a type you did not write: synthesis upward from an expression, contextual typing pushed downward into callbacks, and inference at a generic call site. The central rule is that a type parameter the checker cannot resolve is a hard error, not a silent fallback to a catch-all.
Type inference is how CruftScript works out a type you did not write down. It runs in two directions at once: it computes a type upward from an expression, and it pushes an expected type downward into an expression that has one waiting for it.
const n = 42 // inferred number
const mixed = [1, "a"] // inferred Array<number | string>
[1, 2, 3].map(x => x.toFixed(0)) // x inferred as number from map's callback
The two directions
Synthesis computes a type from an expression on its own. An unannotated const, the value in a return, or an array literal's element type all get their type this way, by looking at what is actually there. An array literal's element type is the best common type of its members, which is why [1, "a"] comes out as Array<number | string>.
Contextual typing goes the other way: when the surrounding code already expects a particular type, that expectation is pushed inward. The common case is an unannotated callback. If you pass an arrow to a function that expects (x: number) => string, the arrow's parameter x inherits the type number from the callee, so you do not have to annotate it yourself.
function each(xs: Array<number>, f: (x: number) => void): void { ... }
each([1, 2, 3], x => x.toFixed(0)) // x is number, supplied by each's signature
Inference at a generic call site
When you call a generic function without writing its type arguments, the checker infers them. It collects candidate types for each type parameter structurally from the arguments you passed, then resolves each parameter to the best common type of its candidates. Candidates that come in as unknown do not pollute a mixed bucket, so an unknown alongside real candidates does not drag the result down to unknown.
function id<T>(x: T): T { return x }
id(42) // T = number
Inference failure is a hard error
This is where CruftScript parts ways with TypeScript. When the checker cannot pin down a type parameter, it does not fall back to a catch-all. It stops and asks you to write the type argument explicitly. Failure takes three forms:
- No candidates for a parameter. Nothing in the call supplied a type for it.
- Conflicting candidates with no best common type. Two arguments demand incompatible types and there is no single type that covers both.
- A return type still mentioning an unresolved parameter. The result would name a type variable that was never resolved.
function pick<T>(): T { ... }
pick() // error: no candidates for T; write pick<Foo>()
function pair<T>(a: T, b: T): [T, T] { ... }
pair(1, "x") // error: conflicting candidates, number vs string
Each of these demands an explicit type argument. TypeScript would quietly resolve a stuck parameter to unknown (or, historically, {}) and carry on. CruftScript treats that silent guess as a bug waiting to happen and refuses it.
Why it works this way
Inference is a convenience: it saves you from annotating types the checker can work out on its own. The danger is that a convenience becomes a place where a type slips through unresolved and the checker papers over it. By making a stuck inference a hard error instead of a silent fallback, CruftScript keeps the convenience without the hole. If the checker cannot prove what a type is, you are told, and you write it down. A function that sits on a value entering or leaving your program is held to a stricter bar still: it must carry explicit parameter and return types, because there is no trustworthy context to infer them from.
Limitations
- A stuck parameter stops the build. There is no fallback to
unknownor any other catch-all. If inference cannot resolve a type argument, you supply it. This is the intended cost of never guessing. - A boundary-facing function needs explicit types. A function whose values enter or leave your program must annotate its parameters and return type; inference will not fill them in for you.
- Variance-aware inference and
NoInferare designed but not built. The rules for steering inference by variance, and theNoInfermarker that blocks a position from contributing candidates, are specified but not yet in the checker.