Language Foundations

Types

Choose types for function inputs and results, name allowed values, and provide types for empty collections.

Choose which values a function accepts and returns, then write those types in its declaration. Use the example to read an inferred type, a union, and a function type.

Install Topaz using First Program. For an introduction to functions and local values, start with Values and Functions.

Check and run a typed function

Save this code as types.tpz in your working folder.

TOPAZ
type Stage = "draft" | "ready"

function describe(stage: Stage, checks: int) -> string {
    "{stage} after {checks} checks"
}

let checks = 3
let current: Stage = "ready"
let render: (Stage, int) -> string = describe

print(render(current, checks))

In that folder, check the file and run it.

BASH
topaz check types.tpz
topaz run types.tpz

The check succeeds with exit code 0. The program prints this line.

Output
ready after 3 checks

Read the input and result types

describe accepts a Stage and an int, then returns a string. The type (Stage, int) -> string describes that same input and result. It lets render store the function and call it later.

Topaz infers int for checks from its initial value, 3. This is type inference. Writing current: Stage supplies the type explicitly, which is called a type annotation.

Stage names the union "draft" | "ready". A union accepts a value from any of its listed alternatives. Here, those alternatives are two string literals, so another string such as "waiting" fails the type check. To fix that error in types.tpz, change the value assigned to current to "draft" or "ready".

A type alias gives an existing type another name. Stage and "draft" | "ready" mean the same type. The alias adds no wrapper around the value.

Choose a type for the job

What you needWhat to use
A local value whose type is clear from its initial valueType inference, as in let checks = 3
Explicit function inputs and resultsType annotations
One of several types or literal valuesA union
A function stored or passed as a valueA function type such as (int) -> bool
Separate types even when their fields or stored values matchA nominal record, enum, or newtype

Nominal types are distinguished by their declarations. For example, two separately declared record types remain different even if they have identical fields. See Records & Nominal Data for construction and matching.

Use lowercase int, float, bool, and string for primitive types. Write the unit type as (), whose value is also (). The spelling unit is not a type alias.

Collections use Array<T>, Map<K, V>, and Set<T>, where the letters stand for types. For example, Array<string> holds strings. Use Option<T> for an expected missing value and Result<T, E> for success or a recoverable error.

Give empty values a type

An empty array has no elements from which to infer a type. None also needs the type of the value that could be present. These declarations supply that information.

let names: Array<string> = []
let selected: Option<int> = None

The first declaration makes an empty string array. The second makes an Option<int> with no value. Function parameters, declared results, and surrounding expressions can also provide the needed type. If the type remains unknown, the checker rejects the value.

Keep the type arguments exact

Write the declared number of type arguments whenever you name a type. Array<string> takes one and Map<string, int> takes two. A primitive type or a type parameter takes none. The same rule applies to aliases and user declarations.

Generic types are invariant. This means that widening a type argument does not make the collection types interchangeable. For example, an Array<int> is not an Array<int | string>.

The standard-library types template, JSONValue, File, Bytes, ByteBuffer, Path, Regex, Match, TOMLValue, URL, Date, BigInt, Decimal, and RoundingMode take no type arguments. File denotes the resource type returned by open. Programs use its operations without accessing its internal representation.

Other type rules

  • int and float do not convert implicitly.
  • Put protocol bounds on the type parameters of named functions. Functions & Generics explains how these bounds permit operations in the function body.
  • Recursive aliases, variance annotations, higher-rank polymorphism, and general reflection are not supported.

Continue to Functions & Generics to use these types in reusable functions. See Null, Option, Result & Faults to choose how to represent missing values and errors.