What you add to a project.
billing.hem compiles to a billing.ts sibling whose first line names its source and hashes its body. Other Hem files import it as ./billing.hem, which compiles to ./billing.ts. Plain TypeScript imports ./billing like any other module, so bundlers, test runners, and editors resolve it with no Hem plugin.
While you write in Hem, the build owns the sibling: it refuses to overwrite an edited sibling, so edit the .hem source instead. Commit siblings so reviewers and downstream tools see real TypeScript, or ignore them and let the build produce them. Each build also writes a manifest that formatters, linters, coverage, and watchers can use to skip generated files.
- Build time
- Hem parses your source, applies its own rules, and prints TypeScript. The type checking is tsc's, against your real tsconfig, with your resolution, libraries, paths, and JSX.
- Edit time
- A language server over LSP. Diagnostics land on the line you wrote rather than the generated one, and a file formatted in your editor is byte-identical to one formatted in CI.
- Run time
- Core syntax adds no runtime imports. Explicit libraries remain application dependencies: @hem-lang/std supplies boxed carriers, and the opt-in Effect extension imports the configured Effect module. Retaining emitted TypeScript lets you leave Hem while keeping any libraries the application uses.
Everything is an expression.
if needs no parentheses and produces a value. Shadowing binds a name again; each step can change its type, from ?string to string to number here. The compiler numbers the names in the TypeScript it emits.- Outside classes, arrows are the only way to write a function, so nothing hoists and
thisnever rebinds - A block after
=>yields its last expression - An
ifwith noelsehas type?T x |> fisf(x)andx |> f(a)isf(x, a), so a pipeline emits ordinary calls and imports nothing
One type for absence.
None. Exact types such as T | null preserve TypeScript contracts; ?T means T | null | undefined, with no wrapper object at runtime. Some and None patterns check absence, and DOM APIs retain their exact types: getElementById returns Element | null.xs[i]is?T, so you can't index an array without handling the missing casematch,?.,??,let-elseand!= Noneeach turn?Tback intoT
Exhaustive pattern matching.
match [state, event] builds no tuple at runtime. Each column is tested on its own, and exhaustiveness is checked across all of them. Delete the last arm and the error lists the combinations you skipped.- Guards, or-patterns, array patterns,
asaliases - Discriminated unions imported from TypeScript match with the same literal and record patterns
- When the type is closed, a missing arm is a compile error that names the case
Common JavaScript mistakes are not valid Hem.
Conditions and the logical operators !, && and || take booleans, never truthy or falsy values. any from imported code becomes unknown.
A call that returns a Promise, an Effect, an Option, or a Result must be used or explicitly discarded, and a statement that computes a value only to drop it is refused. Each rule turns a silent failure into a compile error that shows the fix.
if items.length > 0 { flush(items) }Left out of Hem. What's gone shows what to write instead of each one.
- var
- function
- switch
- == (coercing)
- truthiness
- null
- undefined
- any
- enum
- interface
- namespace
- this (outside classes)
- for..in
- for (;;)
- do..while
- ++ --
- labels
- the comma operator
- declaration merging
Tagged error handling.
error declares a tagged Error subclass, raise throws one, and a postfix catch handles errors by tag. Errors a handler doesn't name are rethrown unchanged, so handlers compose.catchattaches to any expression,awaitincludeduselowers to the platform'susing/await using
Parallel asynchronous execution.
and and they run concurrently. A binding can't refer to another in the same group, so the order they finish in never matters.- Delete the
ands and it is sequential again - The same shape works over Effect, where a failure interrupts its siblings
Effect, when you want it.
raise fails the Effect with a typed error, catch handles its error channel, and use releases on interruption too. The emitted code is ordinary Effect TypeScript.Users!gets a service andfindById(id)!runs an Effect, likeyield*insideEffect.gen- The compiler rejects Effect mistakes TypeScript accepts, such as running an Effect in the wrong place or letting a scope leak
The tools ship with the language.
hem check / build- Check reports diagnostics without writing anything. Build refreshes the generated siblings, removes stale ones, and refuses to overwrite a file you wrote by hand or a generated file someone edited. Build also has a watch mode.
hem fmt- One formatter serves your editor and CI, so both produce byte-identical files.
hem test- Emits siblings, then runs the project's own Vitest with the Vite plugin, so React suites need no separate Hem harness.
hem migrate- Converts TypeScript to Hem only when it can prove the result behaves the same. If any construct in a file can't be converted safely, it leaves the whole file alone.
Vite / VS Code- The Vite plugin refreshes changed siblings in development and builds. The VS Code extension bundles the language server for diagnostics, navigation, formatting, and completions on your Hem source.