Skip to Content

Types

This part covers the full landscape of custom types: nullable types, structs, classes, tuples, props, enums, and more.

NullableT? means "a value or nothing". Non-nullable types are guaranteed non-null at compile time; null checks only make sense on nullable types. The basic syntax (??, ?., if val) is covered in Operators and Conditional Branches; this page focuses on the type-level rules.
Structstruct is a composite type with value semantics: assignment and argument passing copy the whole thing, and changing a copy leaves the original untouched. A good fit for small, self-contained data such as coordinates or config entries.
Datadata declares read-only data: the declaration is the instance, deeply immutable, fully validated at compile time. A good fit for data written directly into code: config tables, tuning values, static atlas descriptions.
TupleA tuple is a fixed-length, heterogeneous, immutable sequence of values. Literal and type share the same shape: (3, "a") has type (int, string). The component count and each component's type are part of the type, so (int, string), (int, int), and (int, string, int) are all distinct types.
Classclass is a composite type with reference semantics: assignment and argument passing share the same object, so changing an alias changes the original. Classes support single inheritance, virtual methods, abstract and static members, and are the primary declaration form for data that carries behavior and identity.
Propprop turns members that read like fields but can carry logic on write into first-class citizens. An auto prop declares in a single line; accessor bodies appear only when needed.
Enumenum declares a closed set of variants. Variants can carry no data or carry a payload; together with match, structured branching over mutually exclusive cases becomes a first-class construct.
Flagsflags declares a set of constants that combine bitwise. Members are physically int bits, but combinations within the same declaration keep the flags static surface, and the method surface is available directly.
Errorerror declares error types. Errors are lightweight values: constructed uniformly with new, flowing along the fail channel; once consumed they are ordinary data — printable, reference-comparable, with a readable payload. This page covers the declaration surface of errors; for the complete story of throwing and consuming see Failure.
GenericsGenerics decouple "the shape of a type" from "the concrete type": types and functions alike take type parameters, and the instantiation is fixed only at the call site. Constraints are expressed with where clauses; the constraint surface is the capability surface.
Protocolprotocol declares a set of member signatures: whoever impls it can flow as the protocol type. It is an interface contract, and also the vehicle for generic constraints.
Mixinmixin is a declarative block of reusable members that is cloned whole into the class hosting it at compile time. It solves the problem of "several unrelated classes need the same set of methods" without occupying the single inheritance slot.
Extensionextension injects new methods into an existing type: the type's source stays untouched, and completion and calls on the method surface behave like native members. Both scalars and user types can be extended.
Anyany holds any value; the cost is that the value's type is unknown at compile time.
Frozenfrozen<T> turns a value into a deeply immutable form: it is deep-copied at construction, cut off from its source, and from then on every write path (indexer, mutation method, field assignment) is intercepted at compile time. The paired ToMutable() is the only way to thaw.
Attributeattribute declares a metadata annotation, and @Name attaches it to a declaration. An attribute adds no behavior to a declaration: it is data written for readers other than the person reading the code — editor panels, serialization markers, hot-reload boundaries, and host injection surfaces are all expressed through it. Declarations and applications are fully validated at compile time; reading goes through reflection at runtime.
typeof and nameofTwo compile-time introspection expressions: typeof(T) gets the Type value of a type, and nameof(x) folds a name into a string constant.
Equality and HashingThe rule for == and != is a language-wide split (ADR-0102): value-semantic types compare by value, reference-semantic types compare by identity. A type that overloads operator == follows the overload; when it does not, this page's rule applies.
Type Aliastypealias gives an existing type a new name.
Last updated on October 11, 2026