Types let the checker answer a practical question before a program runs: which values may cross this boundary? Most local values need no written type. Add one when it explains an important input, result, or domain choice.
Start with one checked program
Save this as types.tpz:
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))Check the boundaries, then run the program:
topaz check types.tpz
topaz run types.tpz
The result is:
ready after 3 checksRead the types from the result
checks has no annotation. Its initializer is an integer, so Topaz infers int. current does have an annotation because the program wants a Stage, not any arbitrary string.
Stage is a transparent alias for the literal union "draft" | "ready". The union says that either listed value is valid and every other string is rejected. The alias gives that choice a useful name, but it does not create a new runtime identity.
render shows a callable type. (Stage, int) -> string means “a function that accepts a Stage and an int, then returns a string.” The named function describe has exactly that shape, so it can be stored in the binding and called later.
The primitive type names are lowercase: int, float, bool, string, and unit (). Common containers use named constructors such as Array<T>, Map<K, V>, and Set<T>. Expected absence and recoverable failure use Option<T> and Result<T, E>.
Choose the shape that states your intent
| Need | Prefer | Why |
|---|---|---|
| An obvious local value | inference | The initializer already communicates the type. |
| A public input, result, or important local boundary | an annotation | The checker and reader see the intended contract directly. |
| One value from a closed set of alternatives | a union or literal-union alias | Each admitted alternative remains explicit. |
| A value that can be called later | a callable type such as (int) -> bool | Parameter and result types travel with the value. |
| Domain identity, fields, or named cases | a record, enum, or newtype | Nominal declarations remain distinct even when their contents look alike. |
A type alias improves vocabulary but remains transparent. Choose nominal data when mixing two structurally similar values would be a real mistake. The Records & Nominal Data page compares record, enum, and newtype construction and matching.
Give empty values enough context
Some values do not carry enough information by themselves. An empty array cannot reveal its element type, and None cannot reveal the missing value type. Supply context through an annotation, a function parameter, a declared result, or another surrounding type:
let names: Array<string> = []
let selected: Option<int> = NoneThese are canonical shapes shown as text here; the runnable example above remains the page’s verified program.
Common mistake: writing the target language’s type spelling
Topaz collection types use Array<T>, Map<K, V>, and Set<T>. Callable types use parentheses around their parameter types, followed by -> and the result type. Keeping these spellings explicit makes checker diagnostics and generated targets agree on the same source contract.
Exact limits
- There is no implicit conversion between
intandfloat. - Generic type parameters are invariant; two different arguments do not become compatible merely because their contents seem related.
- Generic bounds belong only to named function type parameters. Functions & Generics explains the admitted rank-1 form.
- Recursive aliases, variance annotations, higher-rank polymorphism, and broader reflection remain deferred.
- A generic empty value that still has unsolved type parameters is a static error rather than a runtime guess.
If inference, annotations, and mutation are still new, revisit Values and Functions. For choosing among absence, recoverable failure, and faults, continue to Null, Option, Result & Faults.