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

Type System

Perry erases types at compile time, similar to how tsc removes type annotations when emitting JavaScript. However, Perry also performs type inference to generate efficient native code.

Type Inference

Perry infers types from expressions without requiring annotations:

function inferenceBasics(): void {
    let x = 5;           // inferred as number
    let s = "hello";     // inferred as string
    let b = true;        // inferred as boolean
    let arr = [1, 2, 3]; // inferred as number[]

    console.log(`inference: x=${x} s=${s} b=${b} arr_len=${arr.length}`)
}

Inference works through:

  • Literal values: 5number, "hi"string
  • Binary operations: a + b where both are numbers → number
  • Variable propagation: if x is number, then let y = x is number
  • Method returns: "hello".trim()string, [1,2].lengthnumber
  • Function returns: user-defined function return types are propagated to callers
function inferenceFunction(): void {
    function double(n: number): number {
        return n * 2;
    }
    let result = double(5); // inferred as number

    console.log(`inference-function: result=${result}`)
}

Type Annotations

Standard TypeScript annotations work:

interface Config {
    port: number;
    host: string;
}

function annotations(): void {
    let name: string = "Perry";
    let count: number = 0;
    let items: string[] = [];

    function greet(name: string): string {
        return `Hello, ${name}`;
    }

    const cfg: Config = { port: 8080, host: "localhost" }
    console.log(`annotations: ${greet(name)} count=${count} items=${items.length} port=${cfg.port}`)
}

Utility Types

Common TypeScript utility types are erased at compile time (they don’t affect code generation):

type MyPartial<T> = { [P in keyof T]?: T[P] };
type MyPick<T, K extends keyof T> = { [P in K]: T[P] };
type MyRecord<K extends string, V> = { [P in K]: V };
type MyOmit<T, K extends keyof T> = MyPick<T, Exclude<keyof T, K>>;
type MyReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : never;
type MyReadonly<T> = { readonly [P in keyof T]: T[P] };

These are all recognized and erased — they won’t cause compilation errors.

Generics

Generic type parameters are erased:

function identity<T>(value: T): T {
    return value;
}

class Box<T> {
    value: T;
    constructor(value: T) {
        this.value = value;
    }
}

function genericsDemo(): void {
    const box = new Box<number>(42);
    const id = identity<string>("hello")
    console.log(`generics: box.value=${box.value} id=${id}`)
}

At runtime, all values are NaN-boxed — the generic parameter doesn’t affect code generation.

Type Checking with --type-check

For stricter type checking, Perry can integrate with Microsoft’s TypeScript checker:

perry file.ts --type-check

This resolves cross-file types, interfaces, and generics via an IPC protocol. It falls back gracefully if the type checker is not installed.

Without --type-check, Perry relies on its own inference engine, which handles common patterns but doesn’t perform full TypeScript type checking.

Union and Intersection Types

Union types are recognized syntactically but don’t affect code generation:

type StringOrNumber = string | number;

function process(value: StringOrNumber) {
    if (typeof value === "string") {
        console.log(value.toUpperCase());
    } else {
        console.log(value + 1);
    }
}

Use typeof checks for runtime type narrowing.

Type Guards

function isString(value: any): value is string {
    return typeof value === "string";
}

function typeGuardsDemo(): void {
    const x: any = "hello"
    if (isString(x)) {
        console.log(x.toUpperCase());
    }
}

The value is string annotation is erased, but the typeof check works at runtime.

Next Steps