Skip to content

Absence: ?T

The ?T type, where it comes from, the eliminators that discharge it, and how Some and None resolve.

Updated View as Markdown

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 undefined

Some(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 null

Treat 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).

Navigation

Type to search…

↑↓ navigate↵ selectEsc close