Skip to content

Variant, brand, and error declarations

Payload forms, emitted shapes, brand rules, error classes, and the deriving Eq relation.

Updated View as Markdown

Three declaration forms extend type. All three emit ordinary TypeScript that is the public ABI: consumers outside Hem see unions, constructor functions, and classes.

Variants

type Shape = | Circle({ r: number }) | Rect({ w: number, h: number }) | Point

The discriminant is always _tag. Emit is a readonly discriminated union plus one constructor per member.

Payload forms

Form Declaration Emitted member Constructor
None Point { readonly _tag: "Point" } A shared singleton constant
One positional Ident(string) { readonly _tag: "Ident"; readonly value: string } Takes the value
Record Paid({ at: Date }) { readonly _tag: "Paid"; readonly at: Date } Takes a fields object

Record payload fields spread onto the variant object. No detached payload object exists at runtime, which is why a record pattern binds the narrowed variant itself, _tag included.

Multiple positional payloads refuse with HEM2017.

Type parameters

A variant takes type parameters like any other declaration. Constructors infer them from context, and the parameters survive into .d.ts output.

export type Result<T, E> = | Ok(T) | Err(E)

Brands

type UserId = brand string

Emits string & { readonly __brand: "UserId"; readonly __identity: unique symbol } plus an identity constructor UserId(value: string): UserId. The constructor is the identity function at runtime and provides a searchable construction boundary, mirroring variant constructors.

Property Rule
Base type Any non-generic type, including a union
Unwrapping Free: a branded type is a subtype of its base
Construction Required: a bare base value is not assignable to the brand
Pattern form None
Type parameters Refused (HEM2020)
Diagnostic rendering The brand name, not the expanded intersection

brand is contextual in exactly one position, immediately after a type declaration’s =. Everywhere else it is an ordinary identifier, so type X = brand with nothing following aliases a type named brand.

Independent declarations remain distinct even when their names and base types match. Imports, aliases, and reexports of the same declaration preserve its identity. Installed package copies follow TypeScript’s declaration resolver, including package-name/version unification. This intentionally revises the earlier name-only contract; no runtime wrapper or Hem identity registry is involved.

Errors

error QuotaExceeded({ orgId: string })

Declares a nominal tagged error class carrying _tag and extending Error. Declared cause and message fields forward to super. export error exports the class.

Under the Effect extension the same declaration emits Data.TaggedError, which also extends Error and also carries _tag. The two emits are shape-compatible, so instanceof and _tag matching work against both and modules compiled with and without the extension exchange errors freely.

deriving Eq

A variant or error declaration may carry a trailing deriving Eq clause. It generates a comparator named eq<Name> beside the constructors, with visibility mirroring the declaration’s own.

type Tree = | Leaf(number) | Node({ left: Tree, right: Tree }) deriving Eq

The relation is recursive SameValueZero over declared data fields. SameValueZero is === with one correction: NaN equals itself, and -0 equals 0.

What the walk descends into

Field type Comparison
string, boolean, bigint ===
number SameValueZero
T[] Length check, then index-wise recursion through a shared per-file eqArray helper
Tuples Position-wise
?T Both absent is equal; one absent is unequal; otherwise recurse
A brand Resolved to its underlying primitive first
Another variant or error that derives Eq Calls that type’s own comparator, self-reference included
Anything else Reference identity

Functions, Date, RegExp, Map, Set, and imported classes fall into the last row. Two distinct Date values with the same timestamp are not equal under deriving Eq: the relation is structural over shapes the compiler owns, not a deep-equality library.

Dispatch and generics

A variant compares _tag first, then only the matched tag’s declared fields, narrowing the second operand with one as Extract<…> cast. An error has one shape and no tag, so its comparator compares the declared fields only, never message, cause, name, or stack.

A generic variant emits a generic comparator taking one extra trailing comparator parameter per type parameter that appears in payload position.

Scope and refusals

Only variant and error declarations accept the clause. A plain type alias has no constructors to sit beside and no closed field set to walk.

HEM2023 covers two refusals: an unsupported deriving name, and a field typed as a Hem variant or error that does not itself derive Eq. A genuinely foreign type compares by reference with no diagnostic, which is the documented structural boundary.

Normative source

Specification 0.2, §8 (declarations), §9 (errors), §3 (types). The full deriving Eq conformance checklist and worked examples are specified alongside it.

Navigation

Type to search…

↑↓ navigate↵ selectEsc close