Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Supported TypeScript Features

Perry compiles a practical subset of TypeScript to native code. This page lists what’s supported.

Primitive Types

function primitives(): void {
    const n: number = 42;
    const s: string = "hello";
    const b: boolean = true;
    const u: undefined = undefined;
    const nl: null = null;

    console.log(`primitives: n=${n} s=${s} b=${b} u=${u} nl=${nl}`)
}

All primitives are represented as 64-bit NaN-boxed values at runtime.

Variables and Constants

function variables(): void {
    let x = 10;
    const y = "immutable";
    var z = true; // var is supported but let/const preferred

    console.log(`variables: x=${x} y=${y} z=${z}`)
}

Perry infers types from initializers — let x = 5 is inferred as number without an explicit annotation.

Functions

function functionsDemo(): void {
    function add(a: number, b: number): number {
        return a + b;
    }

    // Optional parameters
    function greet(name: string, greeting: string = "Hello"): string {
        return `${greeting}, ${name}!`;
    }

    // Rest parameters
    function sum(...nums: number[]): number {
        return nums.reduce((a, b) => a + b, 0);
    }

    // Arrow functions
    const double = (x: number) => x * 2;

    console.log(`functions: add=${add(2, 3)} greet=${greet("Perry")} sum=${sum(1, 2, 3)} double=${double(5)}`)
}

Classes

class Animal {
    name: string;

    constructor(name: string) {
        this.name = name;
    }

    speak(): string {
        return `${this.name} makes a noise`;
    }
}

class Dog extends Animal {
    speak(): string {
        return `${this.name} barks`;
    }
}

// Static methods
class Counter {
    private static instance: Counter;
    private count: number = 0;

    static getInstance(): Counter {
        if (!Counter.instance) {
            Counter.instance = new Counter();
        }
        return Counter.instance;
    }
}

Supported class features:

  • Constructors
  • Instance and static methods
  • Instance and static properties
  • Inheritance (extends)
  • Method overriding
  • instanceof checks (via class ID chain)
  • Singleton patterns (static method return type inference)

Enums

// Numeric enums
enum Direction {
    Up,
    Down,
    Left,
    Right,
}

// String enums
enum Color {
    Red = "RED",
    Green = "GREEN",
    Blue = "BLUE",
}

const dir = Direction.Up;
const color = Color.Red;

Enums are compiled to constants and work across modules.

Interfaces and Type Aliases

interface User {
    name: string;
    age: number;
    email?: string;
}

type Point = { x: number; y: number };
type StringOrNumber = string | number;
type Callback = (value: number) => void;

Interfaces and type aliases are erased at compile time (like tsc). They exist only for documentation and editor tooling.

Arrays

function arraysDemo(): void {
    const nums: number[] = [1, 2, 3];

    // Array methods
    nums.push(4);
    nums.pop();
    const len = nums.length;
    const doubled = nums.map((x) => x * 2);
    const filtered = nums.filter((x) => x > 2);
    const sum = nums.reduce((acc, x) => acc + x, 0);
    const found = nums.find((x) => x === 3);
    const idx = nums.indexOf(3);
    const joined = nums.join(", ");
    const sliced = nums.slice(1, 3);
    nums.splice(1, 1);
    nums.unshift(0);
    const sorted = nums.sort((a, b) => a - b);
    const reversed = nums.reverse();
    const includes = nums.includes(3);
    const every = nums.every((x) => x > 0);
    const some = nums.some((x) => x > 2);
    nums.forEach((x) => console.log(x));
    const flat = [[1, 2], [3]].flat();
    const concatted = nums.concat([5, 6]);

    // Array.from
    const arr = Array.from([10, 20, 30]);

    // Array.isArray
    const value: any = [1, 2, 3]
    if (Array.isArray(value)) { /* ... */ }

    // for...of iteration
    for (const item of nums) {
        console.log(item);
    }

    console.log(`arrays: len=${len} doubled=${doubled.length} filtered=${filtered.length} sum=${sum} found=${found} idx=${idx} joined=${joined} sliced=${sliced.length} sorted=${sorted.length} reversed=${reversed.length} includes=${includes} every=${every} some=${some} flat=${flat.length} concatted=${concatted.length} arr=${arr.length}`)
}

Objects

function objectsDemo(): void {
    const obj: { name: string; version: number; [k: string]: any } = { name: "Perry", version: 1 };
    obj.name = "Perry 2";

    // Dynamic property access
    const key = "name";
    const val = obj[key];

    // Object.keys, Object.values, Object.entries
    const keys = Object.keys(obj);
    const values = Object.values(obj);
    const entries = Object.entries(obj);

    // Spread
    const copy = { ...obj, extra: true };

    // delete
    delete obj[key];

    console.log(`objects: val=${val} keys=${keys.length} values=${values.length} entries=${entries.length} copy=${copy.extra}`)
}

Destructuring

function destructuringDemo(): void {
    // Array destructuring
    const [a, b, ...rest] = [1, 2, 3, 4, 5];

    const user = { name: "Alice", age: 30, email: "a@example.com", id: 1 }
    const obj = { id: 2, role: "admin", level: 5 }

    // Object destructuring
    const { name, age, email = "none" } = user;

    // Rename
    const { name: userName } = user;

    // Rest pattern
    const { id, ...remaining } = obj;

    // Function parameter destructuring
    function process({ name, age }: { name: string; age: number }) {
        console.log(name, age);
    }

    process(user)
    console.log(`destructuring: a=${a} b=${b} rest=${rest.length} name=${name} age=${age} email=${email} userName=${userName} id=${id}`)
}

Template Literals

function templateLiteralsDemo(): void {
    const name = "world";
    const greeting = `Hello, ${name}!`;
    const multiline = `
  Line 1
  Line 2
`;
    const expr = `Result: ${1 + 2}`;

    console.log(`template-literals: greeting=${greeting} multiline_len=${multiline.length} expr=${expr}`)
}

Spread and Rest

function spreadRestDemo(): void {
    const arr1 = [1, 2]
    const arr2 = [3, 4]
    const defaults = { theme: "light", size: "md" }
    const overrides = { size: "lg" }

    // Array spread
    const combined = [...arr1, ...arr2];

    // Object spread
    const merged = { ...defaults, ...overrides };

    // Rest parameters
    function log(...args: any[]) { /* ... */ }

    log("a", "b", "c")
    console.log(`spread-rest: combined=${combined.length} merged=${merged.size}`)
}

Closures

function closuresDemo(): void {
    function makeCounter() {
        let count = 0;
        return {
            increment: () => ++count,
            get: () => count,
        };
    }

    const counter = makeCounter();
    counter.increment();
    console.log(counter.get()); // 1
}

Perry performs closure conversion — captured variables are stored in heap-allocated closure objects.

Async/Await

async function asyncAwaitDemo(): Promise<void> {
    interface Profile { id: number; name: string }

    async function fetchUser(id: number): Promise<Profile> {
        // The docs example uses fetch(...) here; we inline a synthetic
        // result so the snippet compiles and runs hermetically.
        return { id, name: `user-${id}` }
    }

    const data = await fetchUser(1);
    console.log(`async-await: id=${data.id} name=${data.name}`)
}

Perry compiles async functions to a state machine backed by Tokio’s async runtime.

Promises

async function promisesDemo(): Promise<void> {
    const p = new Promise<number>((resolve, reject) => {
        resolve(42);
    });

    p.then((value) => console.log(value));

    // Promise.all
    const results = await Promise.all([
        Promise.resolve("a"),
        Promise.resolve("b"),
    ]);

    console.log(`promises: results=${results.length}`)
}

Generators

function generatorsDemo(): void {
    function* range(start: number, end: number) {
        for (let i = start; i < end; i++) {
            yield i;
        }
    }

    for (const n of range(0, 10)) {
        console.log(n);
    }
}

Map and Set

function mapSetDemo(): void {
    const map = new Map<string, number>();
    map.set("a", 1);
    map.get("a");
    map.has("a");
    map.delete("a");
    map.size;

    const set = new Set<number>();
    set.add(1);
    set.has(1);
    set.delete(1);
    set.size;

    console.log(`map-set: map_size=${map.size} set_size=${set.size}`)
}

Regular Expressions

function regexDemo(): void {
    const re = /hello\s+(\w+)/;
    const match = "hello world".match(re);

    if (re.test("hello perry")) {
        console.log("Matched!");
    }

    const replaced = "hello world".replace(/world/, "perry");

    console.log(`regex: match=${match !== null} replaced=${replaced}`)
}

Error Handling

function errorsDemo(): void {
    try {
        throw new Error("something went wrong");
    } catch (e: any) {
        console.log(e.message);
    } finally {
        console.log("cleanup");
    }
}

JSON

function jsonDemo(): void {
    const obj = JSON.parse('{"key": "value"}');
    const str = JSON.stringify(obj);
    const pretty = JSON.stringify(obj, null, 2);

    console.log(`json: str_len=${str.length} pretty_len=${pretty.length}`)
}

typeof and instanceof

function typeofInstanceofDemo(): void {
    const x: any = "hello"
    if (typeof x === "string") {
        console.log(x.length);
    }

    const obj: any = new Dog("Rex")
    if (obj instanceof Dog) {
        obj.speak();
    }
}

typeof checks NaN-boxing tags at runtime. instanceof walks the class ID chain.

Modules

ES module syntax is fully supported: named exports, default exports, and re-exports.

The exporting module:

// Named exports
export function helper(x: number): number { return x + 1 }
export const VALUE = 42

// Default export
export default class MyClass {
    name: string
    constructor(name: string) {
        this.name = name
    }
}

The importing module:

// Default + named imports from a sibling module
import MyClass, { helper, VALUE } from "./utils"

// Re-export
export { helper } from "./utils"

BigInt

function bigintDemo(): void {
    const big = BigInt(9007199254740991);
    const result = big + BigInt(1);

    // Bitwise operations
    const and = big & BigInt(0xFF);
    const or = big | BigInt(0xFF);
    const xor = big ^ BigInt(0xFF);
    const shl = big << BigInt(2);
    const shr = big >> BigInt(2);
    const not = ~big;

    console.log(`bigint: result_ok=${result !== null} and_ok=${and !== null} or_ok=${or !== null}`)
}

String Methods

function stringMethodsDemo(): void {
    const s = "Hello, World!";
    s.length;
    s.toUpperCase();
    s.toLowerCase();
    s.trim();
    s.split(", ");
    s.includes("World");
    s.startsWith("Hello");
    s.endsWith("!");
    s.indexOf("World");
    s.slice(0, 5);
    s.substring(0, 5);
    s.replace("World", "Perry");
    s.repeat(3);
    s.charAt(0);
    s.padStart(20);
    s.padEnd(20);

    console.log(`string-methods: ${s.toUpperCase()}`)
}

Math

function mathDemo(): void {
    Math.floor(3.7);
    Math.ceil(3.2);
    Math.round(3.5);
    Math.abs(-5);
    Math.max(1, 2, 3);
    Math.min(1, 2, 3);
    Math.sqrt(16);
    Math.pow(2, 10);
    Math.random();
    Math.PI;
    Math.E;
    Math.log(10);
    Math.sin(0);
    Math.cos(0);

    console.log(`math: floor=${Math.floor(3.7)} sqrt=${Math.sqrt(16)}`)
}

Date

function dateDemo(): void {
    const now = Date.now();
    const d = new Date();
    d.getTime();
    d.toISOString();

    console.log(`date: now_positive=${now > 0}`)
}

Console

function consoleDemo(): void {
    console.log("message");
    console.error("error");
    console.warn("warning");
    console.time("label");
    console.timeEnd("label");
}

Garbage Collection

Perry includes a mark-sweep garbage collector. It runs automatically when memory pressure is detected (~8MB arena blocks), but you can also trigger it manually:

function gcDemo(): void {
    gc(); // Explicit garbage collection
}

The GC uses conservative stack scanning to find roots and supports arena-allocated objects (arrays, objects) and malloc-allocated objects (strings, closures, promises, BigInts, errors).

JSX/TSX

Perry’s parser and HIR understand JSX syntax (parsed via SWC, lowered in crates/perry-hir/src/jsx.rs) and .tsx files link through Perry’s built-in jsx() / jsxs() runtime path. You do not need a local react/jsx-runtime package just to compile TSX.

import { Box, Text } from "perry/tui";

function Greeting({ name }: { name: string }) {
  return <Text>{`Hello, ${name}!`}</Text>;
}

const page = <div className="card"><Greeting name="Perry" /></div>;
const app = <Box><Greeting name="TUI" /></Box>;

JSX elements are transformed to function calls via the jsx() / jsxs() runtime. Perry’s built-in adapter supports HTML-style intrinsic tags, fragments, function components, and compile-time rewrites for perry/tui Box / Text so those TUI JSX forms lower to the same native builders as the function-call form.

Caveat: this is Perry’s TSX runtime, not React DOM or full React reconciler semantics. For perry/ui, or for perry/tui intrinsics whose JSX rewrite has not landed yet, the function-call form remains the canonical native API.

JavaScript (.js) Input

Perry is a TypeScript compiler, but TypeScript is a superset of JavaScript — so Perry also compiles plain JavaScript. .js, .cjs, .mjs, and .jsx files are parsed as JavaScript (decorators, JSX, and import attributes enabled) and lowered through the exact same native pipeline as .ts. No type annotations are required.

perry compile src/main.js -o myapp
./myapp

There are no guarantees for every dynamic JavaScript pattern (the Limitations still apply — no eval, no general dynamic require()), but most plain JavaScript projects compile and run.

Node.js Compatibility

Perry implements a large, real (non-stub) slice of the Node.js standard library — fs, http/https/http2, net/tls, dns/dgram, crypto, stream (+ stream/web), events, child_process, cluster, worker_threads, zlib, process, async_hooks / AsyncLocalStorage, Atomics / SharedArrayBuffer, and the WHATWG web globals (fetch, URL, streams, structuredClone, WebCrypto, …). Against Node’s own test suite (node v26, 53 node:* modules) Perry passes ~97% of cases, with overall Node/TypeScript compatibility around 95%. The per-module surface and remaining gaps are tracked in docs/runtime-parity-gaps.md.

Next Steps