Types and nullability#

Meta: nullability is the page's main attraction. Most readers won't have seen GraphQL-style T! outside of a schema and may misread it as "definitely-null" or "force-unwrap."

Built-in types#

The ! sigil#

Lists, nullability matrix#

written meaning
[T] nullable list of nullable T
[T]! non-null list of nullable T
[T!] nullable list of non-null T
[T!]! non-null list of non-null T

Null propagation#

Flow-sensitive narrowing#

Meta: this section pays for itself — the alternative is writing ! casts everywhere. Make sure the examples include both narrowing inside a branch and narrowing after a guard clause.

Null narrowing#

After a guard:

if (x == null) { return "no value" }
# x is now T! here
print(x.length)

Inside a branch:

if (x != null) {
  # x is T! here
  print("got " + x)
}

Else branch:

if (x == null) { print("no x") } else {
  # x is T! here
}

Loop guards:

loop {
  if (x == null) { break }
  # x is T! after the guard
  x = x.next
}

Diverging constructs are narrowing-aware#

Type narrowing via case#

case (animal) {
  c: Cat => c.purr     # c is Cat!
  d: Dog => d.bark     # d is Dog!
}

Conditional result inference (related)#

Compound conditions#

if (x == null or y == null) { raise "missing" }
# both x and y are T! after the diverging guard
if (maybe != null and other != null) {
  maybe + other   # both T! here
}

Limitations#

When narrowing can't reach the value — a field or call result, or a spot where the checker just can't follow your reasoning — the postfix ! operator is the explicit escape hatch: expr! narrows T to T! and raises at runtime if the value turns out to be null. See Operators.

See also Errors: try, catch, raise (raise/try/catch divergence) and Control flow (guards, loops, case).

Meta: field-narrowing and the and-guard non-narrowing are the two most surprising gaps in practice. Both are documented above with the re-bind-to-a-local workaround.

Type variables#

Type hints / casts: ::#

Meta: :: deserves a worked example showing the difference between a type hint (narrowing/disambiguation, e.g. [] :: [String!]! binding a type variable) and a type cast (coercion, e.g. myUrl :: URL!). The runtime-assertion behavior for non-null casts is a footgun worth a short callout.

Coercion rules#