API Stability Notice

Macroforge is under active development. The API is not yet stable and may change between versions. Some documentation sections may be outdated.

Default

The Default macro generates a static defaultValue() factory method that creates instances with default field values. It works like Rust's #[derive(Default)] trait.

Basic Usage

Before (Your Code)
TypeScript
/** @derive(Default) */
class Config {
    host: string;
    port: number;
    enabled: boolean;

    constructor(host: string, port: number, enabled: boolean) {
        this.host = host;
        this.port = port;
        this.enabled = enabled;
    }
}
After (Generated)
TypeScript
class Config {
    host: string;
    port: number;
    enabled: boolean;

    constructor(host: string, port: number, enabled: boolean) {
        this.host = host;
        this.port = port;
        this.enabled = enabled;
    }

    static defaultValue(): Config {
        const instance = new Config();
        instance.host = '';
        instance.port = 0;
        instance.enabled = false;
        return instance;
    }
}
TypeScript
const config = Config.defaultValue();
console.log(config.host);    // ""
console.log(config.port);    // 0
console.log(config.enabled); // false

Automatic Default Values

Like Rust's Default trait, the macro automatically determines default values for primitive types and common collections:

TypeScript TypeDefault ValueRust Equivalent
string""String::default()
number0i32::default()
booleanfalsebool::default()
bigint0ni64::default()
T[] / Array<T>[]Vec::default()
Map<K, V>new Map()HashMap::default()
Set<T>new Set()HashSet::default()
Datenew Date()
T | null / T | undefinednullOption::default()
Custom typesErrorError (needs impl Default)

Nullable Types (like Rust's Option)

Just like Rust's Option<T> defaults to None, nullable TypeScript types automatically default to null:

Before (Your Code)
TypeScript
/** @derive(Default) */
interface User {
    name: string;
    email: string | null;
    age: number;
    metadata: Record<string, unknown> | null;
}
After (Generated)
TypeScript
interface User {
    name: string;
    email: string | null;
    age: number;
    metadata: Record<string, unknown> | null;
}

export namespace User {
    export function defaultValue(): User {
        return { name: '', email: null, age: 0, metadata: null } as User;
    }
}
TypeScript
const user = User.defaultValue();
console.log(user.name);     // ""
console.log(user.email);    // null (nullable type)
console.log(user.age);      // 0
console.log(user.metadata); // null (nullable type)

Custom Types Require @default

Just like Rust requires impl Default for custom types, Macroforge requires the @default() decorator on fields with non-primitive types:

Before (Your Code)
TypeScript
/** @derive(Default) */
interface AppConfig {
    name: string;
    port: number;
    /** @default(Settings.defaultValue()) */
    settings: Settings;
    /** @default(Permissions.defaultValue()) */
    permissions: Permissions;
}
After (Generated)
TypeScript
interface AppConfig {
    name: string;
    port: number;

    settings: Settings;

    permissions: Permissions;
}

export namespace AppConfig {
    export function defaultValue(): AppConfig {
        return {
            name: '',
            port: 0,
            settings: Settings.defaultValue(),
            permissions: Permissions.defaultValue()
        } as AppConfig;
    }
}

Without @default on custom type fields, the macro will emit an error:

// Error: @derive(Default) cannot determine default for non-primitive fields.
// Add @default(value) to: settings, permissions

Custom Default Values

Use the @default() decorator to specify custom default values for any field:

Before (Your Code)
TypeScript
/** @derive(Default) */
class ServerConfig {
    /** @default("localhost") */
    host: string;

    /** @default(8080) */
    port: number;

    /** @default(true) */
    enabled: boolean;

    /** @default(["info", "error"]) */
    logLevels: string[];

    constructor(host: string, port: number, enabled: boolean, logLevels: string[]) {
        this.host = host;
        this.port = port;
        this.enabled = enabled;
        this.logLevels = logLevels;
    }
}
After (Generated)
TypeScript
class ServerConfig {
    host: string;

    port: number;

    enabled: boolean;

    logLevels: string[];

    constructor(host: string, port: number, enabled: boolean, logLevels: string[]) {
        this.host = host;
        this.port = port;
        this.enabled = enabled;
        this.logLevels = logLevels;
    }

    static defaultValue(): ServerConfig {
        const instance = new ServerConfig();
        instance.host = 'localhost';
        instance.port = 8080;
        instance.enabled = true;
        instance.logLevels = ['info', 'error'];
        return instance;
    }
}
TypeScript
const config = ServerConfig.defaultValue();
console.log(config.host);      // "localhost"
console.log(config.port);      // 8080
console.log(config.enabled);   // true
console.log(config.logLevels); // ["info", "error"]

Interface Support

Default also works with interfaces. For interfaces, a namespace is generated with a defaultValue() function:

Before (Your Code)
TypeScript
/** @derive(Default) */
interface Point {
    x: number;
    y: number;
}
After (Generated)
TypeScript
interface Point {
    x: number;
    y: number;
}

export namespace Point {
    export function defaultValue(): Point {
        return { x: 0, y: 0 } as Point;
    }
}
TypeScript
const origin = Point.defaultValue();
console.log(origin); // { x: 0, y: 0 }

Enum Support

Default works with enums. For enums, it returns the first variant as the default value:

Before (Your Code)
TypeScript
/** @derive(Default) */
enum Status {
    Pending = 'pending',
    Active = 'active',
    Completed = 'completed'
}
After (Generated)
TypeScript
enum Status {
    Pending = 'pending',
    Active = 'active',
    Completed = 'completed'
}

export namespace Status {
    export function defaultValue(): Status {
        return Status.Pending;
    }
}
TypeScript
const defaultStatus = Status.defaultValue();
console.log(defaultStatus); // "pending"

Type Alias Support

Default works with type aliases. For object types, it creates an object with default field values:

Before (Your Code)
TypeScript
/** @derive(Default) */
type Dimensions = {
    width: number;
    height: number;
};
After (Generated)
TypeScript
type Dimensions = {
    width: number;
    height: number;
};

export namespace Dimensions {
    export function defaultValue(): Dimensions {
        return { width: 0, height: 0 } as Dimensions;
    }
}
TypeScript
const dims = Dimensions.defaultValue();
console.log(dims); // { width: 0, height: 0 }

Combining with Other Macros

Source
TypeScript
/** @derive(Default, Debug, Clone, PartialEq) */
class User {
  /** @default("Anonymous") */
  name: string;

  /** @default(0) */
  age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}
TypeScript
const user1 = User.defaultValue();
const user2 = user1.clone();

console.log(user1.toString());    // User { name: "Anonymous", age: 0 }
console.log(user1.equals(user2)); // true