Use ?T for everyday absence in Hem-owned application models. ?T is sugar for T | null | undefined. It is Hem’s native, allocation-free absence
representation: there is no wrapper object at runtime and the emitted type is the union itself.
When preserving an external contract, use its exact T | null or T | undefined type, or reuse its exported type alias. Exact unions are writable throughout Hem, including within generic and conditional type expressions; this interop emphasis is authoring guidance, not a compiler restriction. They preserve which sentinel the contract admits. ?T deliberately admits both; it must not replace a one-sentinel public contract. Nullish value literals remain refused; construct absence with contextual None. Canonical boxed
Option<T> is a separate explicit standard-module carrier for nesting, combinators, must-use
signaling, and stable tagged transport; see Standard Option and Result.
Sources of nullish values
| Source | Example | Resulting type |
|---|---|---|
| A nullable TypeScript API | document.getElementById(id) |
Element | null |
| A lookup that may miss | map.get(key) |
V | undefined |
| Array and record indexing | xs[i] |
T | undefined |
| An optional parameter | (x?: T) => … |
x is T | undefined in the body |
An if without else in value position |
if ok { value } |
T | undefined |
Indexing adds undefined to the value type because Hem’s checker requires noUncheckedIndexedAccess. Unchecked indexing is not writable.
Eliminators
These forms discharge nullish values, including exact one-sentinel unions.
| Form | Spelling | Notes |
|---|---|---|
| Match | match v { | Some(x) => … | None => … } |
Both cases checked for exhaustiveness |
| Optional chaining | v?.field |
TypeScript’s operator, unchanged |
| Defaulting | v ?? fallback |
TypeScript’s operator, unchanged |
| Guard binding | let Some(x) = v else return … |
Ordinary local recovery; the else branch must diverge (HEM2014) |
| Carrier binding | let Some(x) = v |
In a compatible nullish-returning arrow, propagates the exact sentinel |
| Comparison | if v != None { … } |
Narrows v in the true branch |
| Optional call | callback?.(arg) |
Result is R | undefined for an R-returning callback |
The optional call evaluates its receiver or getter once. Arguments evaluate only when the callback is present, and a member callback keeps its this receiver. Void-returning, Promise-returning, and generic callbacks follow the same rule.
The refutable pattern set accepted by let-else is Some, constructors, literals, records, arrays, and as aliases. The same set heads while let, which re-tests the pattern each iteration.
How Some and None resolve
Some and None are type-directed in both pattern and expression position.
As patterns on a nullish scrutinee they lower to nullish checks. As expressions in a nullish context, Some(e) is e itself, and None constructs absence in the expected type: it emits null when null is admitted and undefined is not, and undefined when undefined is admitted. This selects a value for a new construction; it does not normalize existing values.
node.textContent = None // emits null; the DOM setter accepts string | null
let found: ?Element = None // emits undefinedSome(x) written with no expected type refuses with HEM2025. Bare None infers undefined. Choose a representation with an explicit annotation:
| Checked context | Some(value) |
None |
|---|---|---|
T | null |
The value itself | null |
T | undefined or ?T |
The value itself | undefined |
Canonical Option<T> |
A tagged Some object | A tagged None object |
| Configured Effect Option | The transport’s Some constructor | The transport’s None constructor |
In a nullish context, Some(value) does not assert presence. It inserts no wrapper: a nullish payload remains absent when matched. To construct a boxed value explicitly, import canonical Option and use Option.some(value) or Option.none. A lexically bound Some or None, including a user variant constructor, keeps its ordinary meaning and takes precedence over contextual vocabulary.
On canonical boxed Option or configured Effect Option, Some and None instead follow that
checked carrier’s tag and payload representation. The spellings are shared; the ABIs are not.
Changing an annotation can change construction
An annotation affects what contextual None constructs. Widening number | null to ?number admits undefined as well, so the same None expression now emits undefined:
let exact: number | null = None // constructs null
let unified: ?number = None // constructs undefined
let preserved: ?number = exact // retains the existing nullTreat an annotation edit around None as a possible runtime change, especially at an external boundary. None is contextual absence, not a fixed JavaScript literal. When both native sentinels are admitted, construction consistently chooses undefined; bare None also infers undefined.
Passing an existing value through an assignment or a compatible function boundary does not perform this selection again. A carrier binding such as let Some(item) = value likewise returns the original absent value unchanged: an observed null remains null, and an observed undefined remains undefined. Constructing a new None instead of returning the observed value can change that result.
Equality against None
== and != against None emit == null and != null. This is the only loose equality Hem emits, and it is deliberate: one evaluation eliminates both null and undefined.
Everywhere else == emits ===.
Predicates over absence
Prefer a checked predicate inferred from the body, such as (value: unknown) => value == None. Authored is or asserts signatures do not gain permission from the nullish type syntax: unchecked narrowing promises still refuse with HEM2029.
Limits and refusals
A literal ??T does not exist and refuses with HEM2003.
Generic instantiation collapses. Map<string, ?number>.get cannot distinguish “key absent” from “key present, value undefined”, because both are the same unboxed union. Similarly, a generic type Maybe<T> = ?T instantiated with an already nullish T adds no nesting. Use boxed Option<Option<T>> for three distinct states: outer None, Some containing inner None, and Some containing inner Some.
?T is for absence that Hem owns. An externally owned T | null or T | undefined contract is not the same set: widening it to ?T changes the public API. Write the exact union directly in Hem, or reuse an exported library type. Check emitted declarations and an unchanged strict TypeScript caller to verify the public contract.
Normative source
Specification 0.2, §10 (optionals and carriers), §6 (operators), §4 (bindings), §7 (while let).