---
title: "TypeScript interop"
description: "How Hem uses TypeScript types, imports and exports ordinary modules, and preserves platform boundaries."
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.

# TypeScript interop

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.

```hem
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:

```hem
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.

```hem
export type NullableText = string | null
export type OptionalText = string | undefined
export let nullable = (value: NullableText): NullableText => value
export let optional = (value: OptionalText): OptionalText => value
```

The 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:

```ts
export type Result =
  | { ok: true; value: string }
  | { ok: false; error: Error };
```

```hem
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](/reference/pattern-matching) 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.

```hem
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.

```hem
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"`.

```hem
"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](/reference/jsx) 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.

```hem
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.

```hem
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:

```hem
class Choice {
  value: number = if true { 1 } else { 2 }
  choose(flag: boolean): number { return match flag { true => 3 | _ => 4 } }
}
```

```ts emitted
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](/reference/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 `.hem` imports 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 `any` is demoted to `unknown` at the boundary; narrow it before use.
- Hem does not rewrite a foreign API's runtime contract. Preserve exact external unions such as `T | null` directly in Hem or through an existing imported type; widening them to `?T` changes 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](/reference/configuration) documents the TypeScript and JSX options Hem reads.

Source: https://hem.run/reference/typescript-interop/index.mdx
