Narrowing#
Narrowing is how a union becomes one of its members inside a branch.
What narrows#
Construct Example
─────────────────────────────── ────────────────────────────────────
Truthiness of a name if s then
Truthiness of a dotted path if config.name then
not <cond> if not s then ... else ... end
== nil / ~= nil if s ~= nil then
A discriminant field if shape.kind == "circle" then
The is operator if v is Point then
ffi.istype<T>(v) if ffi.istype<Point>(v) then
A predicate function if isPoint(v) then
and / or if a and a.b then
if / elseif chains else-branches accumulate facts
Ternary arms v is Point ? v.x : 0
while cond do the body sees the condition
Guard clauses if not s then return end
never-returning helper calls bail() narrows like an inline errorDiscriminant narrowing also follows a copied local, so binding the value to a new name first does not lose the fact.
What does not narrow#
type(x) == "string" does not narrow. This is the one people expect most:
type is an ordinary function returning an ordinary string, and nothing ties its result back to s. Write s is string.
assert(x) as a statement does not narrow x. It narrows through its return value, because its signature subtracts nil:
Only names and dotted paths narrow. An index like a[i], a call, or any computed expression has no stable key to attach a fact to.
any never narrows. It is already compatible with everything.
Two smaller limits: the falsy side of and proves nothing, and subtracting every member of a union leaves the union alone, since there is no bottom type.
Facts live in a scope#
A narrowed fact dies with the scope that proved it, and assigning to a name clears the facts for that name and everything beneath it:
Predicate functions#
When narrowing cannot see what you know, write a predicate. The return type v is T names a parameter and a type:
The body is trusted. The checker verifies that the name is a parameter (NUPP2109) and that the parameter could hold that type (NUPP2110), and takes the rest on faith.
Guard clauses#
A function that returns never narrows the code after a call to it, the way an inline error does:
The checker infers this for a body whose every path raises, so the never return type is only needed where it cannot see that — an imported C abort, a declaration file with no body, or a loop that never ends. See primitives.
Exhaustiveness#
When every branch of a dispatch over a closed set of literals returns, the checker reports the unhandled members as the exhaustiveness lint. See unions.
Exhaustiveness counts single literal types and unions of them. A union of records is narrowed by a discriminant field instead.