Type Inference
Kestrel infers the types of most things, most of the time. You don't have to write let x: Int = 5 — let x = 5 is enough. But the rules aren't magic; once you've seen this page, you'll know when to expect inference to do its job and when to give it a hand.
What gets inferred
- Local bindings.
letandvarget their type from the right-hand side. - Closure parameters and returns, when the closure is being passed to a function whose parameter type is known.
- Generic type arguments. When you call
identity(42), Kestrel infersT = Intfrom the argument type. - Enum cases via
.Fooshorthand. When the expected type isColor,.Redis enough; you don't have to writeColor.Red.
What doesn't get inferred
- Function parameter types. Always written.
- Function return types. Always written. Omit the
->clause and the function returns()— the compiler won't infer a return type from the body. - Top-level declarations. Module-level constants need an explicit type. (Inference inside a single binding works, but cross-file inference is intentionally out of scope.)
- Empty collection literals.
let xs = []has no signal — writelet xs: [Int] = [].
Literal defaults
When a literal has no surrounding context, Kestrel picks a default:
- Integer literal →
Int - Float literal →
Float - Boolean literal →
Bool - String literal →
String - Character literal →
Char
When there is context, the literal takes that type instead:
let port: UInt16 = 8080; // 8080 is a UInt16 here
When inference can't decide
Sometimes there genuinely isn't enough information — a generic call where the type parameter doesn't appear in the arguments, or a literal that has to be one of several compatible types. The compiler tells you with a "could not infer type" error and asks for a hint:
// let xs = []; // error: could not infer type
let xs: [Int] = []; // ok
let ys = Array[Int](); // also ok
These cases are uncommon in practice; when they show up, write the type and move on.
A useful mental model
Think of inference as the compiler propagating type information out from constants, literals, and function signatures, then unifying everything in the middle. When inference works, it's because that propagation has a single consistent answer. When it doesn't, it's because the propagation either has too many answers or too few — and the fix is to add a type annotation that resolves the ambiguity.
For the deeper semantics — how the solver handles where-clauses, associated types, default conformances — see the compiler internals docs. Day to day, the surface rules above are what you need.