Status: Implemented
Add unknown
and never
types that are inhabited by everything and nothing respectively.
There are lots of cases in local type inference, semantic subtyping,
and type normalization, where it would be useful to have top and
bottom types. Currently, any
is filling that role, but it has
special “switch off the type system” superpowers.
Any use of unknown
must be narrowed by type refinements unless another unknown
or any
is expected. For
example a function which can return any value is:
function anything() : unknown ... end
and can be used as:
local x = anything()
if type(x) == "number" then
print(x + 1)
end
The type of this function cannot be given concisely in current
Luau. The nearest equivalent is any
, but this switches off the type system, for example
if the type of anything
is () -> any
then the following code typechecks:
local x = anything()
print(x + 1)
This is fine in nonstrict mode, but strict mode should flag this as an error.
The never
type comes up whenever type inference infers incompatible types for a variable, for example
function oops(x)
print("hi " .. x) -- constrains x must be a string
print(math.abs(x)) -- constrains x must be a number
end
The most general type of x
is string & number
, so this code gives
a type error, but we still need to provide a type for oops
. With a
never
type, we can infer the type oops : (never) -> ()
.
or when exhaustive type casing is achieved:
function f(x: string | number)
if type(x) == "string" then
-- x : string
elseif type(x) == "number" then
-- x : number
else
-- x : never
end
end
or even when the type casing is simply nonsensical:
function f(x: string | number)
if type(x) == "string" and type(x) == "number" then
-- x : string & number which is never
end
end
The never
type is also useful in cases such as tagged unions where
some of the cases are impossible. For example:
type Result<T, E> = { err: false, val: T } | { err: true, err: E }
For code which we know is successful, we would like to be able to
indicate that the error case is impossible. With a never
type, we
can do this with Result<T, never>
. Similarly, code which cannot succeed
has type Result<never, E>
.
These types can almost be defined in current Luau, but only quite verbosely:
type never = number & string
type unknown = nil | number | boolean | string | {} | (...never) -> (...unknown)
But even for unknown
it is impossible to include every single data types, e.g. every root class.
Providing never
and unknown
as built-in types makes the code for
type inference simpler, for example we have a way to present a union
type with no options (as never
). Otherwise we have to contend with ad hoc
corner cases.
Add:
never
, inhabited by nothing, andunknown
, inhabited by everything.And under success types (nonstrict mode), unknown
is exactly equivalent to any
because unknown
encompasses everything as does any
.
The interesting thing is that () -> (never, string)
is equivalent to () -> never
because all
values in a pack must be inhabitable in order for the pack itself to also be inhabitable. In fact,
the type () -> never
is not completely accurate, it should be () -> (never, ...never)
to avoid
cascading type errors. Ditto for when an expression list f(), g()
where the resulting type pack is
(never, string, number)
is still the same as (never, ...never)
.
function f(): never error() end
function g(): string return "" end
-- no cascading type error where count mismatches, because the expression list f(), g()
-- was made to return (never, ...never) due to the presence of a never type in the pack
local x, y, z = f(), g()
-- x : never
-- y : never
-- z : never
Another bit of complexity budget spent.
These types will be visible to creators, so yay bikeshedding!
Replacing any
with unknown
is a breaking change: code in strict mode may now produce errors.
Stick with the current use of any
for these cases.
Make never
and unknown
type aliases rather than built-ins.