luau/rfcs/none-and-unknown-types.md
2022-03-28 15:34:44 -05:00

2.9 KiB

Add never and unknown types

Summary

Add unknown and never types that are inhabited by everything and nothing respectively.

Motivation

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.

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) -> ().

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>.

The unknown case is useful in cases where the user of an API must use type casing to use the API safely. 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"
    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.

These types can be defined in current Luau, but only quite verbosely:

  type never = number & string
  type unknown = nil | number | boolean | string | {} | Instance | (never...)->(unknown...)

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.

Design

Add:

  • a type never, inhabited by nothing, and
  • a type unknown, inhabited everything.

Use them, rather than any where appropriate. Ideally, we would then never infer any.

Drawbacks

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.

Alternatives

Stick with the current use of any for these cases.

Make never and unknown type aliases rather than built-ins.