---
title: "Safety by construction"
description: "The value, operator, mutability, and boundary rules that make common JavaScript mistakes explicit."
image: "https://hem.run/social/hem.png"
---

> Documentation Index
> Fetch the complete documentation index at: https://hem.run/llms.txt
> Use this file to discover all available pages before exploring further.

# Safety by construction

Hem disallows several common JavaScript failure modes at compile time. These rules require values and operations to be stated explicitly.

## Booleans and equality

Conditions, prefix logical `!`, `&&`, and `||` require boolean operands. Non-boolean values such as numbers, strings, arrays, or objects cannot be used as conditions; write explicit comparisons or presence checks instead. Inside an Effect block, postfix `!` is a bind operator, not logical negation.

```hem
let hasItems = items.length > 0
if hasItems && enabled { flush(items) }
```

`==` compiles to JavaScript's strict equality (`===`), and `!=` compiles to strict inequality (`!==`). Loose equality is not supported. The only exception is comparison with `None`: `value == None` and `value != None` compile to `value == null` and `value != null`, checking for both `null` and `undefined` in a single evaluation.

## Numeric operators

Binary `+` and `+=` perform arithmetic only. Both operands must be `number`, or both must be `bigint`. String concatenation and mixed numeric operations are rejected with error HEM2021. For string concatenation, use template literals. Unary `+` also requires a `number` operand and does not perform type coercion.

```hem
let total = price * quantity
let message = `Total: ${total}`
```

This prevents unintentional type conversions from runtime string values and requires explicit conversions, such as `Number(value)`.

## Absence and checked indexing

Hem refuses authored `any` types; exact `null` and `undefined` types retain their TypeScript meaning. In core Hem their value literals remain refused; bounded class members retain native absence values. `?T` represents the unboxed optional type `T | null | undefined` for Hem types. Array index access is checked: under `noUncheckedIndexedAccess`, `items[i]` may be absent. Check the returned value before using it; a bounds check alone does not establish its presence.

```hem
let names: string[] = ["Ada"]
let first: ?string = names[0]
let greeting = match first {
| Some(name) => `Hello, ${name}`
| None => "Hello"
}
```

Do not use `?T` to widen external `T | null` or `T | undefined` types. Write the exact union directly in Hem, or reuse the external library's exported type.

Postfix `!` does not assert that a value is present. HEM2004 suggests handling
absence, capturing and checking the read, or requiring a stronger input type.
Its Promise and Effect guidance is conditional: awaiting a Promise does not
prove that its resolved value is present. Repairs must preserve evaluation
order and count, so no automatic rewrite is offered.

## Binding and value mutability

`let` creates an immutable binding and compiles to `const`. `let mut` permits
replacing the binding's value. It does not make the value's fields or elements
writable.

Object properties and class fields are readonly by default. Declare a field
with `mut` to permit writes:

```hem
let point: { x: number, mut y: number } = { x: 1, y: 2 }
point.y += 1
```

Array and tuple types are readonly by default. Use `mut T[]` or
`mut [A, B]` for writable storage. For nested arrays, `mut (T[])[]` permits
changing the outer array; `(mut T[])[]` permits changing the inner arrays.
Global `Array<T>` annotations also receive readonly views; imported aliases
retain their declared permissions.

Tuple mutation must preserve required positions and declared slot types.
HEM2033 refuses destructive methods, length writes, and indexed writes that
cannot establish this. Compatible element replacement and direct appends to a
required-prefix tuple with a compatible rest element remain available.
Extracting an unsafe mutator does not make it safe. HEM2030 also refuses exposing
a tuple through a mutable-array interface that could remove its required head.
Use an ordinary mutable array when freely changing length is intended.

An application can describe nonemptiness with an ordinary readonly tuple alias
and construct it from a known first value. Copy explicitly when a result must
be independent of its source container; a shallow copy preserves existing
element identities. Native `map` and `filter` keep their usual types and runtime
behavior, and filtering can return an empty array. Hem adds no collection
runtime module, ownership marker, automatic copy, or special nonempty inference.

## Fresh values and existing aliases

Inferred record and array literals receive readonly views. Fresh children
receive their own readonly views; existing children keep their declared
permissions and identity. This is not general deep immutability.

Element and field types widen normally. An empty inferred array has element
type `never`; annotate its element type when needed. An explicit mutable
contract permits fresh mutable construction.

For `if`, `match`, and `try` results, fresh branches receive their views
before their types are combined. Selecting an existing mutable value preserves
its permissions. A local binding does not inherit an enclosing callback's
mutable return contract.

Spreads retain the permissions of existing children. Later fields replace
earlier fields in source order. Destructuring rest creates a new container
with a readonly view; it does not freeze or copy the objects stored inside it.

These rules also apply in the supported class subset. Class member bodies use
TypeScript-shaped syntax; they do not accept all Hem expression forms. Boolean conditions, numeric `+`, foreign `any` demotion, immutable plain `let`, assertion refusal, and must-use apply to methods, initializers, and nested callbacks. `let mut` enables local mutation and `let _ = expression` explicitly discards a value. See
[TypeScript interoperability](/reference/typescript-interop).

## Sharing does not grant write permission

HEM2030 rejects checked transfers that make an existing readonly value writable
or widen the writes accepted by an existing mutable value. This includes
nested record properties, array elements, tuple slots, and indexed records.

A readonly parameter is a view, not a copy or freeze. Other aliases can still
change the object. Passing a readonly array to a library parameter declared
mutable is refused, even if you believe the function only reads it. Use a
supported readonly API or explicitly construct fresh mutable storage when
copying is appropriate. A new outer container does not remove the write
restrictions of shared children.

Generic constraints do not establish permission to write every possible
instantiation. Keep the generic identity type, expose a readonly view, or
construct fresh storage from checked values.

These checks are not an ownership system or a proof of every TypeScript
generic, mapped-type, or foreign-object relationship. Imported declarations
remain trusted contracts.

## Property checks and intervening calls

HEM2031 reports a property read that relies on a check an intervening operation
may have invalidated. Even a function with readonly parameters can mutate
another alias.

Check the property again after the operation, or capture the checked primitive
before it. Capturing the object itself does not preserve the values of its
properties. A recheck must guard the later read; ignoring its result does not
restore evidence.

Calls are not the only operations that can execute user code. Object spreads
can invoke getters or proxy traps; iteration and destructuring can invoke
iterators and defaults. Property writes can invalidate checks through shared
aliases. Evaluation order matters: a primitive read before a call is already
captured, but a property read after that call needs current evidence.

Branches and loops preserve evidence only on paths that justify the read.
Catch handlers need fresh checks. Cleanup can invalidate evidence before
control reaches its destination.

These checks cover property and indexed reads in supported control-flow
forms, not every closure or dynamic access pattern. TypeScript may also reject
a read independently. HEM2031 offers guidance rather than an automatic edit:
moving a read earlier can change program behavior.

## Values must be used

Hem reports diagnostic HEM2018 when an expression statement produces a value that is discarded without being used. This applies to pure expressions (such as comparisons) and calls that return carriers: `Promise`, `Effect`, canonical `Option` or `Result`, or configured boxed carriers. Calls with void or side-effecting return types, such as `console.log`, `array.push`, and `map.set`, are permitted.

```hem
let saved = await save(document)
let _ = metrics.record("saved")
```

To intentionally discard a value, use `let _ = expression`. This compiles to `void (expression);`, evaluating the expression while marking the result as deliberately discarded. Must-use checking catches accidental omission; explicit discard acknowledges the choice rather than proving that the work completed safely.

Discard does not await a Promise or handle its rejection. A discarded Promise can still reject without a handler. Await it, handle its rejection, or give it to an explicit supervision mechanism when completion matters. Discarding an Effect description does not run it, and discarding a Result does not handle its failure.

## Type assertions need checked evidence

The HEM2028 check includes assertions in class method bodies and field initializers.

Project checking rejects assertions that turn `unknown` into a concrete type,
including the narrowing step in `value as unknown as string` (HEM2028). Use a
checked condition or runtime decoder instead. Widening and literal `as const`
assertions remain available; `satisfies` checks conformance without supplying
unchecked evidence. Pattern aliases are unaffected.

An `as` or `satisfies` expression cannot itself be an assignment or update target (HEM2032). This
includes compound assignments, destructuring targets, and `for...in` or
`for...of` targets in class bodies. For example, `(value as number) = 2` cannot
bypass a `const` binding. Assign to the original binding or property so its
mutability and accepted type are checked. Assertions used to read values,
including computed assignment keys, remain subject to the usual evidence checks.

These checks do not establish universal soundness. Imported declarations remain
trusted contracts, and foreign code can violate those contracts.

## Foreign values become `unknown`

Hem uses TypeScript for type checking, but expressions typed as `any` are converted to `unknown` at the boundary. The compiler inserts a cast and reports diagnostic HEM2001. Values typed as `unknown` must be narrowed using `typeof`, `instanceof`, type predicates, or `match` before they can be used.

## What these rules prevent

- A truthy number or string taking an unintended branch.
- Loose equality converting unrelated values before comparing them.
- String concatenation or mixed `number`/`bigint` arithmetic hiding a type error.
- An unchecked array read being used as if an element were guaranteed.
- A shared record or class field changing without an explicit mutable declaration.
- A dropped promise, carrier, or comparison concealing missing work.
- An `any` value escaping a foreign boundary without a narrowing step.

## Normative source

Specification 0.2, §3 (types), §4 (bindings), §6 (operators and must-use), §10 (absence), §13 (interop), and §15 (removed constructs).

## From TypeScript practices to Hem

The [TypeScript practices guide](https://github.com/cursor/plugins/blob/main/pstack/skills/typescript-best-practices/SKILL.md) and its [patterns](https://github.com/cursor/plugins/blob/main/pstack/skills/typescript-best-practices/references/patterns.md) mix type guarantees with application and team choices. Hem preserves that distinction:

| Practice | Hem contract |
| --- | --- |
| Discriminated states | Variant declarations and structural unions support checked narrowing; choosing a useful domain model remains the author's job. Optional-field records remain legal. |
| Domain brands | Declaration-owned identity prevents independent brands from mixing. Construction returns the original value; it does not validate it. |
| Constructive models | Required tuple positions express a known first element. Supported mutations and writable transfers cannot erase that guarantee. |
| Simplest total input | Keep ordinary arrays for operations that handle emptiness; require a tuple only where a first element is needed. No collection runtime is added. |
| External unknowns | Foreign `any` expressions become `unknown`; narrow or parse before use. |
| Existing schemas | Prefer the application's schema library and infer its output type. Hem generates no validators. |
| Assertions | HEM2028 refuses unchecked narrowing, including `unknown` to a concrete domain. Widening and `as const` remain available. |
| Narrowing | Match discriminants, `in`, `typeof`, and `instanceof` use TypeScript's checked facts. Captured reads can preserve evidence across calls. |
| Predicates | Authored `is`/`asserts` signatures are refused; checked inference and imported predicates remain available. Foreign implementations are trusted. |
| Complete matches | Supported closed domains need every case or an intentional fallback. Missing cases error; redundant arms warn. |
| Conformance | `satisfies` checks a value without authorizing an unchecked narrowing cast. |
| Boundary parsing | Prefer a public type and checked parser with a local constructor. Explicit constructor exports remain legal. Boundary placement and wire-version policy belong to the application. |
| Reusing types | Schema inference and ordinary utility types remain available; avoiding duplicate models is an authoring preference. |
| Named arguments | Object parameters are available and preferred where they clarify calls; positional parameters remain legal. |
| Real verification | Compiler, emitted-runtime, declaration, installed-package, and browser checks establish different claims. This is a development practice, not a syntax rule. |
| Telemetry | Structured logging and useful context remain application policy. Hem does not prohibit `console.log`. |

A required head proves `items[0]`, not an arbitrary numeric index. A plain numeric duration can still be negative. Choose validation and construction that establish the actual domain constraint instead of assuming a type name proves it.

The [recurring invoice preview](https://github.com/ozanvos/hem/tree/main/examples/recurring-invoice-preview) demonstrates the validated-domain recipe with React, Zod, and date-fns. Its schema creates local amount/count brands, the schedule constructs its first item explicitly, and its success/error UI uses a complete structural match. Readonly describes access; an independent snapshot still needs an explicit shallow copy, and that copy retains references to nested objects. Foreign declarations remain trusted contracts.

Source: https://hem.run/reference/safety/index.mdx
