Primitive types#

These names, and only these, resolve as bare builtin types:

 Name       Means
 ─────────  ──────────────────────────────────────────────────────
 any        The gradual type; compatible with everything
 unknown    The top type; everything fits it, it fits nothing else
 never      The bottom type; fits everything, nothing fits it
 nil        The nil singleton
 boolean    true or false
 string     A Lua string
 number     A LuaJIT double
 integer    A number known to be integral
 table      Any table shape; gradual in both directions
 thread     A coroutine
 userdata   Userdata
 float      A C float; widens to number
 cdata      Any cdata value
 cstring    const char *
 voidptr    void *
 int8       Sized C integers, signed and unsigned
 int16
 int32
 int64
 uint8
 uint16
 uint32
 uint64

metatable<T>, ctype<T>, carray<T>, owned<T>, borrowed<T>, and pinned<T> are constructors rather than names — each needs a type argument, and bare metatable is an unknown type name.

unknown, the top type#

any is the gradual opt-out: it is compatible with everything in both directions, silently, which is exactly right for code that has not been annotated yet. unknown is the sound alternative, for a value whose type genuinely is not known — a JSON decode, a pcall result, reflection over an undeclared table:

Anything fits into unknown, but it fits nowhere else on its own — reading a field, calling it, comparing it against a typed value, all need it narrowed or cast first, the same as any other concrete type that is not what the operation wants:

It names in a function type the same as any other type, in parameter or return position:

A variadic parameter typed unknown takes anything, the way bare ... does, but gives each extra argument a type to narrow before use instead of none at all:

Equality against a literal narrows unknown the same way it narrows any other type, so a chain that checks it against every member of a literal-type union narrows all the way there:

Use unknown where any would otherwise stand for "I have not looked at this value yet, and every use of it should have to say how."

never, the bottom type#

never is uninhabited: no value has it. That makes it fit anywhere any type is wanted (there being no value of it to violate the expectation) while nothing but never itself fits into it. It is what a function that always raises, exits, or loops forever returns:

A call to a never-returning function leaves the block it stands in the same way an inline error does, which is what lets the guard clause above narrow x. The checker infers this for a body whose every path raises, whether or not it says never — the return type is only needed where the checker cannot see that for itself: a loop that never ends, or a declaration with no body to read, such as local error: function(msg: any, level: number?): never in the prelude. Declaring never on a function that does return is an ordinary return-type mismatch, since nothing but never fits never.

A function type carries it in return position with no special syntax beyond the name:

A never variadic parameter takes no extra arguments at all — nothing but never fits never, so any value offered there is refused:

Because it fits anywhere, a never-returning call also satisfies a literal type, the same as any other declared return:

Numbers#

integer is a subtype of number. The reverse is not true, and there is no implicit downcast:

The sized C integers behave differently. Any numeric source is accepted into a float or a sized-integer slot, as in C:

So number → integer is refused while number → int32 is allowed. The sized types are a C boundary, where truncation is the expected arithmetic; integer is a claim about a Lua value. In a strict file — a .nupp one, or any file under --strict — narrowing a wider numeric into a small sized type raises the lossy-narrowing lint, and the suggested fix is an explicit as.

Literals type as you would expect: 1 is an integer literal, 1.5 and 1e3 are number, 1LL is int64, 1ULL is uint64, 0xff is integer.

Unions and optionals#

T? is sugar for T | nil. Unions flatten, deduplicate, and sort, so A | B and B | A are the same type.

For a union to be assignable, every member has to fit. For a value to fit a union, it has to match some member.

An optional field on a shape is both nullable and omissible — leaving it out satisfies it:

Write A | B with spaces. A||B lexes as the single || operator.

Collections#

 Form            Means
 ──────────────  ────────────────────────────────────────────────
 {T}             Lua array, one-based, dense
 {T, U}          Tuple, fixed positions
 {[K]: V}        Map with an explicit key type
 {x: T, y: U}    Inline shape
 T[4]            C array of fixed length, zero-based
 T[?]            C array of unspecified length, zero-based

Reading a map yields V?, because a key may be absent. Reading an array yields T rather than T? — a pragmatic choice, since almost every array read in practice is in range.

{T} and T[N] are different types: one is a Lua table, the other is cdata.

Pointers#

 Form       Means
 ─────────  ────────────────────────────────
 T*         Pointer to T
 T*?        Pointer that may be NULL

Pointers are invariant in their pointee. nil is not a T*; the diagnostic says so and names T*? as the fix. A struct value is accepted where struct* is wanted, matching LuaJIT's automatic address-of.

Every pointer that import-c generates is nullable, because a C header does not say which pointers may be NULL.

const#

A read-only view. A mutable value satisfies a const parameter; a const value does not satisfy a mutable one.

This is unrelated to the const binding modifier, which makes a local immutable:

Literal types#

A string or boolean literal is a type:

false exists as a type so that T | false narrows usefully. A literal is assignable to its base type, and to any union that lists it. A union of literals is the closed set other languages spell enum — see unions.

Type aliases#

An alias is transparent — it introduces a name, not a new nominal identity, so Id and uint32 are interchangeable. Aliases may be generic, and may refer to each other in any order; an alias defined in terms of itself is NUPP2115.

Function types#

Parameter names are optional. A multiple return needs parentheses in type position.

Parameters are contravariant and returns are covariant, as usual. A function that takes fewer parameters is usable where more are supplied, because ignoring arguments is ordinary Lua; taking more is an error unless the target is variadic.

Function parameters and results are represented as value sequences. Fixed, homogeneous, generic, and correlated sequences are described in Type packs.