Features That Disappear at Build Time
Some TypeScript features are designed to keep your code readable and safe — and then vanish completely when Cuttlefish writes the C++. The board ends up running lean, direct code, with no extra layers or overhead. You write readable TypeScript; the board runs tight C++.
rawCpp() — Put Raw C++ In a Specific Spot
The rawCpp() function drops a line of raw C++ right where you call it. The TypeScript itself never runs — the string just becomes part of the generated C++.
import { rawCpp } from '@typecad/hal';
// This TypeScript never runs — the string becomes C++ at the call site
rawCpp("PORTB |= (1 << PB5)");
// rawCppExpr() is the expression form — for where a value is expected
import { rawCppExpr } from '@typecad/hal';
const v: number = rawCppExpr("PORTB & (1 << PB5)");Good for the rare case the hardware library doesn’t cover — direct register access, a line of inline assembly, or platform-specific code.
What happens at build time
- Cuttlefish spots the
rawCpp()call. - The string is placed directly into the generated C++ at that position.
- The call itself is removed — there’s no function call left at runtime.
What happens during type-checking/testing
rawCpp() is a no-op function (function rawCpp(code: string): void {}). Your TypeScript tests and type-checker simply skip over it.
include() — Add a C++ Library
The include() function adds a #include line (a C++ library import) to the generated output.
import { include } from '@typecad/hal';
include("<Wire.h>"); // → #include <Wire.h>
include('"mylib.h"'); // → #include "mylib.h"Calling include("<Wire.h>") twice only produces one #include.
board() — Read a Value From the Board Definition
The board() function looks up a value from the board definition and puts it directly into the C++ as a fixed constant.
import { board } from '@typecad/hal';
const boardName = board("name");
// Resolves to a compile-time constant from the generated board manifestResolvable paths
The path argument follows the generated board manifest’s constant map:
board("id") // → "esp32s3_devkitc"
board("name") // → "ESP32-S3 DevKitC"
board("zephyr.soc") // → "esp32s3"
board("pins.all.13.name") // → "D13"
board("pins.all.13.number") // → 13
board("pins.all.13.capabilities.pwm") // → true/false
board("peripherals.i2c.count") // → number of I2C controllersAt build time the path is resolved from the generated board manifest and put into the C++ as a constant. In a plain TypeScript test, the call returns undefined.
callback() — Mark a Function as a Callback
The callback() directive tells Cuttlefish that a function should be registered as a callback (for example, for an interrupt) rather than called directly.
import { callback } from '@typecad/hal';
callback(() => {
// This becomes an interrupt handler in C++
led.toggle();
});⚙️ Advanced details — what the optimizer does for you
These transformations all happen at build time. You don’t write any of it — it’s just useful to know what’s going on under the hood.
Phantom ownership types
Owned<T>, Shared<T>, and Mutable<T> are phantom types that exist only in the TypeScript type system for build-time validation. They evaporate completely in the C++ output:
Owned<T> — single ownership
The value is emitted as-is with no wrapper. The transpiler tracks ownership transfers (moves) and reports use-after-move errors.
let buf: Owned<Buffer> = new Buffer(64);
let other = buf; // ownership transferred (move)
// buf is now invalid — use-after-move diagnosticC++ output:
Buffer buf(64);
auto other = buf;
// No wrapper types, no reference counting
// The transpiler tracks ownership at compile time onlyShared<T> — immutable borrow
Adds const in C++ for compiler-enforced immutability. The transpiler prevents assignment to Shared variables.
const ref: Shared<Buffer> = buf;
// ref.write() → C++ const method (compiler enforces read-only)C++ output:
const Buffer& ref = buf;Mutable<T> — mutable borrow
Emitted as a regular reference. The transpiler enforces exclusivity — no other borrows can exist while a Mutable is active.
const mut: Mutable<Buffer> = buf;
mut.write(data); // Allowed — mutable borrowC++ output:
Buffer& mut = buf;No runtime cost
- No wrapper classes
- No reference counting
- No garbage collection
Shared<T>adds only aconstqualifier — the C++ compiler enforces immutability- All tracking happens at build time through the ownership validator
Pin handle erasure
TypeScript pin objects become direct function calls or register writes in the C++ output. The HAL class layer does not exist at runtime:
// TypeScript — typed, safe
import { GPIO } from '@typecad/hal';
import { LED } from '@typecad/board';
const led = new GPIO(LED, GPIO.OUTPUT);
led.toggle();// C++ — no HAL class, no indirection
gpio_pin_configure_dt(&__tc_dt_led, GPIO_OUTPUT);
gpio_pin_toggle_dt(&__tc_dt_led);The pin’s devicetree spec is resolved from the board definition at build time; the call is the Zephyr driver function with the spec inlined. The flags token (GPIO.OUTPUT ↔ GPIO_OUTPUT) is a TypeScript-only name mapping — no conversion code is generated.
Enum narrowing
The transpiler knows the full, closed value range of every enum at emit time — something the C++ compiler cannot recover. It picks the narrowest underlying type that fits:
enum State { Off, On, Blinking } // values 0–2// C++ — narrowed from the default int to the smallest type that fits
enum class State : uint8_t { Off, On, Blinking };Non-negative ranges that fit in a byte narrow to uint8_t; ranges within [-128, 127] narrow to int8_t. The narrowest-type pick is made once at emit time and stamped into the enum definition — no runtime conversion anywhere.
let → const promotion
The transpiler performs whole-program reassignment analysis — it tracks every assignment to every let binding across all functions and scopes. When it proves a let is never written after initialization, it emits const in C++ automatically:
let threshold = 500; // never reassigned anywhere in the program// C++ — promoted to const, enabling ROM placement and constant folding
const int threshold = 500;This is information the C++ compiler cannot recover across translation units. The inverse demotion (const → non-const when a member is mutated) also applies automatically.
Automatic volatile inference
The classic embedded bug — a global written in an ISR and read in loop() becomes an infinite loop under -Os because the compiler caches it in a register. The transpiler knows which callbacks are interrupt handlers and which globals they write, so it marks shared variables volatile without any user annotation:
let flag = false;
function setup() {
button.onInterrupt(GPIO.INT_EDGE_FALLING, () => { flag = true; });
}
function loop() {
if (flag) { flag = false; }
}// C++ — flag auto-marked volatile
volatile bool flag = false;The volatile keyword is emitted automatically. This eliminates an entire bug class that the C++ compiler cannot detect — “this function runs in interrupt context” is a call-graph property the transpiler sees but g++ does not.
Tree-shaking (removing unused code)
The transpiler performs dead-code elimination by default. Unused enums, classes, type aliases, and variables are removed from the C++ output.
Tree-shaking flags
| Flag | Effect |
|---|---|
--no-tree-shake | Disable all tree-shaking |
--keep-unused-enums | Preserve unused enum definitions |
--keep-unused-classes | Preserve unused class definitions |
--keep-unused-types | Preserve unused type aliases |
--keep-unused-variables | Preserve unused top-level variables |
--entry-point <name> | Additional entry point to keep (repeatable) |
How it works
- The transpiler builds a call graph from the internal representation.
- It identifies entry points (
main()and any functions it calls). - Symbols not reachable from any entry point are marked for removal.
- The C++ emitter skips removed symbols entirely.
Entry point detection is controlled by the platform strategy — main() is always the entry point.
Free-function prototypes
When top-level code calls helper functions defined later in the file, the transpiler automatically inserts forward declarations before main():
// TypeScript — definition after usage
import { Time } from '@typecad/hal';
led.toggle();
Time.sleep(500);
function blink() {
led.toggle();
}// C++ — forward declaration inserted
void blink(); // Auto-generated forward declaration
int main() {
gpio_pin_toggle_dt(&__tc_dt_led);
k_msleep(500);
blink();
}
void blink() {
gpio_pin_toggle_dt(&__tc_dt_led);
}Default arguments go on the forward declaration, not the definition, so they’re available throughout the file.
On This Page