Algebraic data types, compiling to the discriminated unions TypeScript consumers already understand:
export type PaymentState =
| Pending
| Paid({ at: Date, method: string })
| Refunded({ reason: string })export type PaymentState =
| { readonly _tag: "Pending" }
| { readonly _tag: "Paid"; readonly at: Date; readonly method: string }
| { readonly _tag: "Refunded"; readonly reason: string };
export const Pending: PaymentState = { _tag: "Pending" };
export const Paid = (fields: { readonly at: Date; readonly method: string }): PaymentState => ({ _tag: "Paid", ...fields });
export const Refunded = (fields: { readonly reason: string }): PaymentState => ({ _tag: "Refunded", ...fields });A member carries no payload, one positional payload, or a record whose fields spread onto the variant. The _tag discriminant deliberately matches the Effect ecosystem’s convention, so Part II’s tooling consumes Hem variants natively, and so does any hand-written TypeScript.
Variants take type parameters like anything else. Application-specific states stay just as direct:
export type Validation<T, E> =
| Valid(T)
| Invalid(E)export type Validation<T, E> =
| { readonly _tag: "Valid"; readonly value: T }
| { readonly _tag: "Invalid"; readonly value: E };
export const Valid = <T, E>(value: T): Validation<T, E> => ({ _tag: "Valid", value });
export const Invalid = <T, E>(value: E): Validation<T, E> => ({ _tag: "Invalid", value });match stays exhaustive over a generic variant exactly as it is over a concrete one (§8), construction infers its type parameters from context the same way any generic call does, and the type parameters survive into .d.ts output for consumers outside this file.
Hem’s canonical Option and Result are explicit standard-module imports rather than variants each
application reinvents; §19 gives them their stable cross-package ABI and carrier-binding rules.
Brands. A variant gives you a new shape; a brand gives an existing type a name tsc won’t let you confuse with another value of the same shape. A UserId and an OrderId are both strings, but passing one where the other belongs is exactly the bug a nominal wrapper exists to catch:
type UserId = brand stringtype UserId = string & { readonly __brand: "UserId"; readonly __identity: unique symbol };
const UserId = (value: string): UserId => value as UserId;Constructing a brand is an identity operation at runtime (UserId("u_42") evaluates to "u_42"), matching variant constructor conventions. Unwrapping is free, because UserId is a subtype of string. Assigning a raw string where a UserId is required produces a type error, and the compiler diagnostic references the brand name rather than the intersection type.
Structural equality: deriving Eq. Two variant values are equal when they carry the same tag and their fields are equal. Because === performs reference equality checks, comparing complex types would otherwise require custom comparison logic. deriving Eq writes it for you:
export type PaymentState =
| Pending
| Paid({ at: Date, method: string })
| Refunded({ reason: string })
deriving Eqalongside §7’s union and constructors, this emits a comparator:
export const eqPaymentState = (a: PaymentState, b: PaymentState): boolean => {
if (a._tag !== b._tag) {
return false;
}
switch (a._tag) {
case "Pending": {
return true;
}
case "Paid": {
const _hemB = b as Extract<PaymentState, { readonly _tag: "Paid" }>;
return a.at === _hemB.at && a.method === _hemB.method;
}
case "Refunded": {
const _hemB = b as Extract<PaymentState, { readonly _tag: "Refunded" }>;
return a.reason === _hemB.reason;
}
}
};The relation is recursive SameValueZero: ===, except that NaN equals itself and -0 equals 0. It traverses compiler-defined types, handles recursive definitions, and descends into nested variants that also derive Eq.
Foreign types follow different equality semantics. Anything genuinely foreign, a function or a Date or an imported class, compares by reference. This is a structural derive, not a deep-equality library, so two Date values with the same timestamp are not equal here. An error declaration derives the same way, comparing its declared fields and nothing inherited from Error.
The variants, brands, and errors reference has the full walk table, the generic form, the visibility rule, and both HEM2023 refusals.