Datatypes and basic patterns
A datatype introduces a nominal type with a closed set of constructors. Use it when a value may have one of several meaningful shapes.
Declare constructors
Section titled “Declare constructors”Constructors may have no payload, positional payloads, or named payloads:
datatype Message Quit Text(String) Move{x: Int, y: Int}endThere is no punctuation between constructors. Construct values with an
explicit Type::Constructor path:
datatype Message Quit Text(String) Move{x: Int, y: Int}end
func initial_message() -> Message do Message::Move{x: 0, y: 0}endConstructor names need to be unique only inside their own datatype.
Match with switch
Section titled “Match with switch”A switch evaluates its target once and considers cases from top to bottom:
datatype Message Quit Text(String) Move{x: Int, y: Int}end
func describe(message : Message) -> String do switch message case Message::Quit then "quit" case Message::Text(text) then text case Message::Move{x, y} then "move " ++ x.to_string() ++ "," ++ y.to_string() endendThe checker requires the cases to be exhaustive. Every case body must produce
one common result type. Bindings such as text, x, and y exist only in
their own case body.
Wildcards and binders
Section titled “Wildcards and binders”_ matches any value without creating a binding. A lower name matches any
value and binds it:
datatype Status Ready Waiting(Int) Failed(String)end
func is_ready(status : Status) -> Bool do switch status case Status::Ready then true case _ then false endendUse a wildcard only when all ignored cases are intentionally equivalent. A fully enumerated switch is more robust when different alternatives should remain visible.
Single-constructor datatypes
Section titled “Single-constructor datatypes”A single-constructor datatype is useful when a value needs nominal identity:
datatype UserId UserId(Int)end
func raw_id(id : UserId) -> Int do id._0endDirect payload field access is available because the static type determines
the only possible constructor. Named payloads use their declared field names.
For a multi-constructor datatype, inspect the value with switch instead.
Extend a datatype
Section titled “Extend a datatype”A datatype may include all constructors of one or more base datatypes:
datatype Base Aend
datatype Derived extends Base BendA Base value widens to Derived without a wrapper. Inherited constructors
remain qualified by their declaring owner, so code constructs and matches
Base::A, not Derived::A:
func name(value : Derived) -> String do switch value case Base::A then "a" case Derived::B then "b" endendA base must be monomorphic: it declares no type parameters and is referenced without type arguments. The derived datatype may still declare its own type parameters, and every application widens to the same base:
datatype ErrorBase Commonend
datatype DerivedError[T] extends ErrorBase Extra(T)endMatching must cover the complete set: direct constructors plus every inherited one.
Extern constructors
Section titled “Extern constructors”A datatype can describe already existing JavaScript values instead of
Noodle-allocated ones. Mark the datatype extern, name the companion
classifier with @js_recognizer, and give every constructor an explicit
tag with @js_tag — either an integer or a non-empty string, one shape per
datatype:
@js_recognizer("recognize_blob")extern datatype Blob @js_tag(1) BlobendThe companion lives in the module’s .extern.mjs file and maps a host value
to its tag:
export const recognize_blob = (value) => { if (value instanceof Blob) return 1; throw new Error("unrecognized Blob value");};Extern constructors participate in matching, widening, and exhaustiveness like
ordinary constructors, but they can never be constructed: values arrive only
through extern func returns or host calls. The alias in
original @ Blob::Blob binds the original host object with no wrapper, and
matching a frozen-prototype host class works normally — the compiler never
writes to host prototypes.
A named payload on an extern constructor projects read-only host properties
during matching. A trailing .. in the declaration marks the projection
open, and every matching pattern must then also end with ..:
@js_recognizer("recognize_blob")extern datatype Blob @js_tag(1) Blob{size: Int, ..}end
func size_of(value : Blob) -> Int do switch value case Blob::Blob{size: size, ..} then size endendExactly one positional payload is allowed when it carries @unboxed: the
binder receives the host value itself with the declared type — no property
is read, no wrapper is allocated. The standard library Json type uses this
shape for its Bool, Num, Str, Arr, and Obj constructors.
Generic datatypes, guards, nested patterns, aliases, and or-patterns are covered in Advanced pattern matching and Generic programming.
Next: error handling.