Skip to content
Noodle
InstallLearnPlayground
GitHub

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.

Constructors may have no payload, positional payloads, or named payloads:

datatype Message
Quit
Text(String)
Move{x: Int, y: Int}
end

There 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}
end

Constructor names need to be unique only inside their own datatype.

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

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

_ 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
end
end

Use a wildcard only when all ignored cases are intentionally equivalent. A fully enumerated switch is more robust when different alternatives should remain visible.

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

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

A datatype may include all constructors of one or more base datatypes:

datatype Base
A
end
datatype Derived extends Base
B
end

A 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"
end
end

A 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
Common
end
datatype DerivedError[T] extends ErrorBase
Extra(T)
end

Matching must cover the complete set: direct constructors plus every inherited one.

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) Blob
end

The 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
end
end

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