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.

Debug

The Debug macro generates a toString() method that produces a human-readable string representation of your class.

Basic Usage

Before (Your Code)
TypeScript
/** @derive(Debug) */
class User {
    name: string;
    age: number;

    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }
}
After (Generated)
TypeScript
class User {
    name: string;
    age: number;

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

    toString(): string {
        const parts: string[] = [];
        parts.push('name: ' + this.name);
        parts.push('age: ' + this.age);
        return 'User { ' + parts.join(', ') + ' }';
    }
}
TypeScript
const user = new User("Alice", 30);
console.log(user.toString());
// Output: User { name: Alice, age: 30 }

Field Options

Use the @debug field decorator to customize behavior:

Renaming Fields

Before (Your Code)
TypeScript
/** @derive(Debug) */
class User {
    /** @debug({ rename: "userId" }) */
    id: number;

    name: string;

    constructor(id: number, name: string) {
        this.id = id;
        this.name = name;
    }
}
After (Generated)
TypeScript
class User {
    id: number;

    name: string;

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

    toString(): string {
        const parts: string[] = [];
        parts.push('userId: ' + this.id);
        parts.push('name: ' + this.name);
        return 'User { ' + parts.join(', ') + ' }';
    }
}
TypeScript
const user = new User(42, "Alice");
console.log(user.toString());
// Output: User { userId: 42, name: Alice }

Skipping Fields

Use skip: true to exclude sensitive fields from the output:

Before (Your Code)
TypeScript
/** @derive(Debug) */
class User {
    name: string;
    email: string;

    /** @debug({ skip: true }) */
    password: string;

    /** @debug({ skip: true }) */
    authToken: string;

    constructor(name: string, email: string, password: string, authToken: string) {
        this.name = name;
        this.email = email;
        this.password = password;
        this.authToken = authToken;
    }
}
After (Generated)
TypeScript
class User {
    name: string;
    email: string;

    password: string;

    authToken: string;

    constructor(name: string, email: string, password: string, authToken: string) {
        this.name = name;
        this.email = email;
        this.password = password;
        this.authToken = authToken;
    }

    toString(): string {
        const parts: string[] = [];
        parts.push('name: ' + this.name);
        parts.push('email: ' + this.email);
        return 'User { ' + parts.join(', ') + ' }';
    }
}
TypeScript
const user = new User("Alice", "[email protected]", "secret", "tok_xxx");
console.log(user.toString());
// Output: User { name: Alice, email: [email protected] }
// Note: password and authToken are not included
Security
Always skip sensitive fields like passwords, tokens, and API keys to prevent accidental logging.

Combining Options

Source
TypeScript
/** @derive(Debug) */
class ApiResponse {
  /** @debug({ rename: "statusCode" }) */
  status: number;

  data: unknown;

  /** @debug({ skip: true }) */
  internalMetadata: Record<string, unknown>;
}

All Options

OptionTypeDescription
renamestringDisplay a different name in the output
skipbooleanExclude this field from the output

Interface Support

Debug also works with interfaces. For interfaces, a namespace is generated with a toString function:

Before (Your Code)
TypeScript
/** @derive(Debug) */
interface Status {
    active: boolean;
    message: string;
}
After (Generated)
TypeScript
interface Status {
    active: boolean;
    message: string;
}

export namespace Status {
    export function toString(self: Status): string {
        const parts: string[] = [];
        parts.push('active: ' + self.active);
        parts.push('message: ' + self.message);
        return 'Status { ' + parts.join(', ') + ' }';
    }
}
TypeScript
const status: Status = { active: true, message: "OK" };
console.log(Status.toString(status));
// Output: Status { active: true, message: OK }

Enum Support

Debug also works with enums. For enums, a namespace is generated with a toString function that displays the enum name and variant:

Before (Your Code)
TypeScript
/** @derive(Debug) */
enum Priority {
    Low = 1,
    Medium = 2,
    High = 3
}
After (Generated)
TypeScript
enum Priority {
    Low = 1,
    Medium = 2,
    High = 3
}

export namespace Priority {
    export function toString(value: Priority): string {
        const key = Priority[value as unknown as keyof typeof Priority];
        if (key !== undefined) {
            return 'Priority.' + key;
        }
        return 'Priority(' + String(value) + ')';
    }
}
TypeScript
console.log(Priority.toString(Priority.High));
// Output: Priority.High

console.log(Priority.toString(Priority.Low));
// Output: Priority.Low

Works with both numeric and string enums:

Source
TypeScript
/** @derive(Debug) */
enum Status {
  Active = "active",
  Inactive = "inactive",
}

Type Alias Support

Debug works with type aliases. For object types, fields are displayed similar to interfaces:

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

export namespace Point {
    export function toString(value: Point): string {
        const parts: string[] = [];
        parts.push('x: ' + value.x);
        parts.push('y: ' + value.y);
        return 'Point { ' + parts.join(', ') + ' }';
    }
}
TypeScript
const point: Point = { x: 10, y: 20 };
console.log(Point.toString(point));
// Output: Point { x: 10, y: 20 }

For union types, the value is displayed using JSON.stringify:

Source
TypeScript
/** @derive(Debug) */
type ApiStatus = "loading" | "success" | "error";
TypeScript
console.log(ApiStatus.toString("success"));
// Output: ApiStatus("success")