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:
5→number,"hi"→string - Binary operations:
a + bwhere both are numbers →number - Variable propagation: if
xisnumber, thenlet y = xisnumber - Method returns:
"hello".trim()→string,[1,2].length→number - 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
- Supported Features — Complete feature list
- Limitations — What’s not supported