The Transpiler

Hal doesn't interpret your TypeScript — it compiles it: parse to an AST, build an IR, run the validators, emit C++, and hand the result to the toolchain. Source maps connect every emitted line back to yours, so C++ errors point at TypeScript.

reference

type mapping

The TypeScript you write decides the C++ you get. Plain, predictable rules — nothing dynamic survives to the board, because there's nowhere to put it.

Example first

TypeScriptC++
numberint — widened to long long when values overflow
boolean / stringbool / const char* — no heap strings
uint8_t, int32_t, size_t...Pass straight through — declare them in TS
ArraysFixed C arrays (Zephyr) or std::vector (native)
ObjectsAnonymous structs — struct _config_t { ... }
Set / Map / Recordstd::set / std::map
Unionsstd::variant — | null drops the null side
?? and ?.Helpers that preserve 0/false and missing objects
Template literalsFixed char buffers + snprintf with inferred format specifiers
enum-like unionsenum class : uint8_t — narrowed to the smallest fitting integer
functions

build-time directives

Escape hatches that disappear at build time. They're real functions when you type-check or test in Node — and become raw C++, an include line, or a board constant in the output.

Example first

native.ts
import { include, rawCpp, rawCppExpr, board, callback } from '@typecad/hal';

include('hal/my_driver.h');

// statement form — emitted verbatim
rawCpp('my_driver_calibrate(&hdev);');

// expression form — usable inline
const id = rawCppExpr<number>('my_chip_id()');

// compile-time constants from the board manifest
const flashKb = board('boardDetails.flashKb');

// mark a function as an interrupt callback
callback(() => {
  count++;
});

board() board(path: string): unknown

ParameterTypeUnits / valuesDescription
pathstring—Dot path into the board manifest — 'boardDetails.flashKb', 'pins.all.13.capabilities.pwm', 'peripherals.i2c.count'

Returns undefined in plain Node, a compile-time constant in the emitted C++ — size buffers and branch on hardware facts without runtime cost.

reference

what gets erased and promoted

The abstractions cost nothing at runtime — the transpiler removes them while keeping the program's meaning, and promotes what it can prove into cheaper code.

Example first

FeatureWhat the build does with it
Pin handlesled.toggle() → gpio_pin_toggle_dt(&__tc_dt_led) — devicetree resolved at build time
Ownership typesOwned/Shared/Mutable erase — Shared just adds const; no wrappers, no refcounting
let → constWhole-program analysis promotes never-reassigned lets to C++ const — ROM and constant folding
volatileGlobals shared with interrupts get volatile automatically
throwCompiles to a deliberate halt loop on Zephyr — there are no exceptions
Tree-shakingUnreachable code never reaches the .cpp — reachability from the entry point

The validators

Every build runs the full validator set against the IR — the mistakes that would be a debug session on hardware are build errors here. Off-board (simulator) or skipped with --skip-type-check, they're your call.

  • Pin capabilities and pin-mode configuration
  • Peripheral validation and peripheral ownership (take/release)
  • Pin alias conflicts and unsafe pins
  • Interrupt safety — which operations block inside an ISR
  • Reentrancy detection for shared state
  • ADC range checks against the board data
  • Unit suspicion (millis vs micros class mistakes)
  • Memory budget — statics + estimated stack vs the board’s RAM
  • Blocking delays in loops
  • Network usage — HTTP without a radio, weak AP passwords
  • Ownership analysis — the Owned/Shared/Mutable checks
  • Try/catch validation
© 2026 typeCAD — code-defined electronics github