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 }) | PointThe 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 stringEmits 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 EqThe 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.