TypeScript provides Hem’s type checker and output format. Hem applies source-level syntax rules and transformations, then passes the resulting TypeScript code to the TypeScript compiler. Hem and TypeScript modules interact across standard ECMAScript module (ESM) boundaries.
Importing TypeScript
You can import typed npm packages directly. Hem checks types against the package’s resolved .d.ts declaration files without requiring FFI bindings, wrappers, or custom definition files.
import { desc } from "drizzle-orm"
let recent = (db: DrizzleD1Database) =>
db.select().from(scans).orderBy(desc(scans.createdAt.getSQL())).limit(20)Type expressions use TypeScript’s type syntax and pass through directly. Hem maps compiler diagnostics back to the original source locations, including multiline union and conditional types.
Exporting validated domains
Named exports support export type { Amount }, type aliases such as export type { Amount as InvoiceAmount }, and mixed lists such as export { type Amount, parseInvoice }. The same forms can reexport from another module. Type-only exports expose no runtime constructor.
A validated domain keeps raw construction local and uses the application’s schema library:
import { z } from "zod"
type Amount = brand number
let schema = z.object({
amount: z.number().positive().transform((value) => Amount(value)),
})
type Invoice = z.infer<typeof schema>
export type { Amount, Invoice }
export let parseInvoice = (input: unknown) => schema.safeParse(input)The schema establishes the constraint before constructing the brand. Ordinary Hem and TypeScript consumers receive the inferred domain type through the parser; they cannot call the local constructor through a type-only export. Identity-only domains can explicitly export their raw constructor with export type Id = brand string or an ordinary value export. Hem does not generate validation from erased types.
Imported unions and narrowing
Keep exact foreign nullability
?T accepts both null and undefined. An API that accepts only one needs its exact type: write T | null or T | undefined directly in Hem. These types pass through unchanged, including generic arguments and conditional types. No TypeScript alias sidecar is required. Nullish value literals remain refused; None selects the sentinel admitted by its checked context.
export type NullableText = string | null
export type OptionalText = string | undefined
export let nullable = (value: NullableText): NullableText => value
export let optional = (value: OptionalText): OptionalText => valueThe emitted declarations keep null and undefined distinct. Reuse a library’s exported type when it names the same contract; ReturnType<Headers["get"]>, an imported field’s indexed-access type, or Parameters<typeof operation>[0] also retain precision. Keep that type owner independent of the implementation being replaced to avoid circular inference. Inspect the signature and compile an unchanged strict TS caller; do not broaden its accepted inputs to ?T merely to clear a diagnostic.
Narrow an imported union
Imported discriminated unions remain standard TypeScript unions. You can narrow them using match, typeof, instanceof, and the in operator; TypeScript checks the narrowing.
For example, api.ts can export this union:
export type Result =
| { ok: true; value: string }
| { ok: false; error: Error };import type { Result } from "./api"
let message = (result: Result): string => match result {
| { ok: true, value } => `ok: ${value}`
| { ok: false, error } => `error: ${error.message}`
}Structural record matching supports a defined subset of shapes. Finite literal-discriminant unions can be exhausted by whole-variant record patterns; open object domains still require a fallback _ arm. See the pattern matching reference for supported patterns and restrictions.
Predicates and assertion functions
Imported predicates and assertion functions remain usable as trusted library contracts. In Hem-authored code, explicit is and asserts signatures are rejected with HEM2029: their signatures alone do not prove their implementations. Let the checker infer a predicate from a checked expression, or return a checked result.
Arrow and type-alias parameter lists are checked for these signatures in their
constraints and defaults. Referencing an imported predicate type remains allowed; fields named
is or asserts are ordinary properties, not predicate signatures.
Class methods and arrow-valued class fields are checked too. A method can use checker-inferred narrowing, but cannot supply an unchecked predicate or assertion signature of its own.
let isString = (value: unknown) => typeof value == "string"
let value: unknown = "hello"
let text = if isString(value) { value } else { "fallback" }This distinction does not verify foreign implementations. Runtime schema decoders and imported validators remain explicit trust boundaries, without requiring duplicate Hem bindings.
Tagged templates
Tagged template literals pass directly to the tagged template function. Hem does not transform tag functions or interpolated expressions.
let query = sql`select * from users where id = ${id}`Tags, template string segments, and interpolated arguments retain their standard TypeScript types and runtime behavior.
Framework directives
A prologue of double-quoted strings at the top of a file is preserved before imports and declarations in the emitted output. This supports directives such as React’s "use client", "use server", and "use strict".
"use client"
import { useState } from "react"A React directive placed after an import, declaration, or statement produces diagnostic HEM2024 to prevent it from becoming an inert string literal. Function-level directive prologues and alternative directive syntaxes are not supported; manage those in TypeScript.
JSX and React types
JSX compiles directly to .tsx. React and the project’s configured jsxImportSource provide JSX type checking. The JSX reference covers Hem JSX expression and attribute syntax, generated TSX output, and optional property handling.
Classes for platform protocols
Classes are supported for APIs that require them, such as Cloudflare Workers Durable Objects, web custom elements, and Error subclasses. Fields are readonly by default; mut makes a field writable. Constructors, methods, static, override, single extends inheritance, this, super, and type parameters retain their TypeScript semantics.
export class Counter extends DurableObject<Env> {
mut count = 0
fetch(req: Request): Response {
this.count += 1
return Response.json({ count: this.count })
}
}Keep application logic in ordinary Hem functions
Use the class to satisfy the platform’s constructor, inheritance, and receiver requirements. Keep larger transformations in ordinary Hem functions and call them from a small method. This gives that logic ordinary Hem syntax and formatting without making every class-body placement a separate language rule.
let describeCount = (count: number): string =>
if count == 0 { "empty" } else { `count: ${count}` }
class CounterView {
mut count = 0
describe(): string {
return describeCount(this.count)
}
}Small expressions inside methods remain supported as described below. Class bodies combine native statements with selected Hem expression regions; moving a function’s body into a method is not a syntax-preserving refactor. Fields and defaults have stricter placement rules, and member bodies are preserved by the formatter. Prefer a supported Hem helper when a placement refuses; moving an unchecked operation into TypeScript does not make it safe.
Safety and supported placements
Class methods, initializers, and nested callbacks enforce the same safety rules as core Hem: boolean conditions and logical operands, numeric +, foreign any demotion to unknown, readonly access, refinement invalidation, and must-use. Postfix value! and definite-assignment field! assertions are refused. A property introduced by a destructuring default can still be inferred as unknown; declare its optional source type when the public result needs a specific type. A fallback alone does not establish the type of a present property.
| Class-local form | Behavior |
|---|---|
let value = expression |
Immutable binding; emits const. |
let mut value = expression |
Mutable binding; emits let. |
const value = expression |
Retained immutable native binding. |
let _ = expression |
Evaluates once and intentionally discards the value. |
var |
Refused; choose let or let mut. |
null / undefined values |
Retained native absence values with exact type contracts. Core Hem uses None. |
switch, C-style for, do, labels, local function declarations |
Retained native control flow. Conditions still require boolean values. |
Comma expressions, ++ / -- |
Retained native evaluation; discarded carriers obey must-use and updates require writable storage. |
| Hem expressions in returns, initializers, call arguments, and pipes | Use ordinary Hem lowering and safety rules. |
Methods can use Hem expressions in returns, local initializers, call arguments, and pipes. Pure if expressions also work in fields and parameter defaults:
class Choice {
value: number = if true { 1 } else { 2 }
choose(flag: boolean): number { return match flag { true => 3 | _ => 4 } }
}class Choice {
readonly value: number = true ? 1 : 2
choose(flag: boolean): number {
{
if (flag === true) {
return 3;
} else {
return 4;
}
}
}
}
export {};Native const, immutable let, explicit mutable let mut, C-style loops, and nested functions retain native control flow and scopes. var is refused; let _ intentionally discards a value. Bare class calls obey must-use, so Promise.resolve(1) needs a use or an intentional let _ discard. Collection storage still needs readonly or explicit mut permission. Nested native parameters and local bindings shadow module names normally.
Expressions that need generated statements can hoist before a standalone native local declaration or inside a return/call statement. A field, parameter default, loop head, or grouped initializer cannot host that prelude: HEM3003 recommends an ordinary Hem helper instead. The formatter preserves member body text. Other Hem statement forms in native control flow receive an owned refusal at the Hem syntax. Keep break/continue in native statements outside structural Hem expressions. A class member cannot borrow an outer Effect generator or loop.
Decorators, access modifiers (public, private, protected), and getters/setters are not supported. Use the supported class subset or a separately compiled TypeScript module. Imported declarations remain trusted contracts, not a way to make a refused unsafe operation checked. Constructor parameter properties are excluded by the erasable-output contract: use explicit fields and constructor assignments. See language boundaries.
JSX inside a bounded class member receives HEM3003. Put JSX in an ordinary Hem helper outside the class, which owns TSX emission, and call that helper from the member.
The sibling ABI
hem build outputs foo.ts alongside foo.hem, or foo.tsx when the source contains JSX. Relative imports ending in .hem are rewritten to .ts in the compiled output. A .ts file can import a compiled .hem module, and a Hem file can import .ts files using standard module resolution. export default and ESM syntax are supported; CommonJS is not.
Emitted files serve as the public API: unions, constructors, functions, classes, and constants compile to standard TypeScript. You can publish emitted .ts(x) and .d.ts files for TypeScript consumers. Files without exports receive an emitted export {} to prevent declarations from entering global scope.
Boundary limits
- Direct
.hemimports across tsconfig project boundaries produce HEM3004. Use emitted modules and declarations at package boundaries; direct cross-project Hem source resolution is not supported. - TypeScript remains responsible for checking opaque type expressions and foreign declarations.
- An expression typed
anyis demoted tounknownat the boundary; narrow it before use. - Hem does not rewrite a foreign API’s runtime contract. Preserve exact external unions such as
T | nulldirectly in Hem or through an existing imported type; widening them to?Tchanges the public API. - Class returns, initializers, and call statements support Hem expressions. Statement-producing field, parameter-default, loop-head, and grouped initializers require an ordinary Hem helper; other structural Hem/native statement mixtures receive HEM3003.
- Output is erasable-syntax-clean: it contains no enums, namespaces, or parameter properties, so it can run under Node type stripping and TypeScript’s
erasableSyntaxOnly.
Normative source
Specification 0.2, §1 (compilation model), §3 (types), §5 (functions), §11 (JSX), §12 (classes), and §13 (modules and interop). The configuration reference documents the TypeScript and JSX options Hem reads.