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.

PartialEq

The PartialEq macro generates an equals() method for field-by-field structural equality comparison. This is analogous to Rust’s PartialEq trait, enabling value-based equality semantics instead of reference equality.

Generated Output

TypeGenerated CodeDescription
ClassclassNameEquals(a, b) + static equals(a, b)Standalone function + static wrapper method
EnumenumNameEquals(a: EnumName, b: EnumName): booleanStandalone function using strict equality
InterfaceinterfaceNameEquals(a: InterfaceName, b: InterfaceName): booleanStandalone function comparing fields
Type AliastypeNameEquals(a: TypeName, b: TypeName): booleanStandalone function with type-appropriate comparison

Comparison Strategy

The generated equality check:

  1. Identity check: a === b returns true immediately
  2. Field comparison: Compares each non-skipped field

Type-Specific Comparisons

TypeComparison Method
PrimitivesStrict equality (===)
ArraysLength + element-by-element (recursive)
DategetTime() comparison
MapSize + entry-by-entry comparison
SetSize + membership check
ObjectsCalls equals() if available, else ===

Field-Level Options

The @partialEq decorator supports:

  • skip - Exclude the field from equality comparison

Example

Before (Your Code)
/** @derive(PartialEq, Hash) */
class User {
    id: number;
    name: string;

    /** @partialEq({ skip: true }) @hash({ skip: true }) */
    cachedScore: number;
}
After (Generated)
class User {
    id: number;
    name: string;

    cachedScore: number;

    static equals(a: User, b: User): boolean {
        return userEquals(a, b);
    }

    static hashCode(value: User): number {
        return userHashCode(value);
    }
}

export function userEquals(a: User, b: User): boolean {
    if (a === b) return true;
    return a.id === b.id && a.name === b.name;
}

export function userHashCode(value: User): number {
    let hash = 17;
    hash =
        (hash * 31 +
            (Number.isInteger(value.id)
                ? value.id | 0
                : value.id
                      .toString()
                      .split('')
                      .reduce((h, c) => (h * 31 + c.charCodeAt(0)) | 0, 0))) |
        0;
    hash =
        (hash * 31 +
            (value.name ?? '').split('').reduce((h, c) => (h * 31 + c.charCodeAt(0)) | 0, 0)) |
        0;
    return hash;
}

Generated output:

class User {
    id: number;
    name: string;

    cachedScore: number;

    static equals(a: User, b: User): boolean {
        return userEquals(a, b);
    }

    static hashCode(value: User): number {
        return userHashCode(value);
    }
}

export function userEquals(a: User, b: User): boolean {
    if (a === b) return true;
    return a.id === b.id && a.name === b.name;
}

export function userHashCode(value: User): number {
    let hash = 17;
    hash =
        (hash * 31 +
            (Number.isInteger(value.id)
                ? value.id | 0
                : value.id
                      .toString()
                      .split('')
                      .reduce((h, c) => (h * 31 + c.charCodeAt(0)) | 0, 0))) |
        0;
    hash =
        (hash * 31 +
            (value.name ?? '').split('').reduce((h, c) => (h * 31 + c.charCodeAt(0)) | 0, 0)) |
        0;
    return hash;
}

Equality Contract

When implementing PartialEq, consider also implementing Hash:

  • Reflexivity: a.equals(a) is always true
  • Symmetry: a.equals(b) implies b.equals(a)
  • Hash consistency: Equal objects must have equal hash codes

To maintain the hash contract, skip the same fields in both PartialEq and Hash:

Before (Your Code)
/** @derive(PartialEq, Hash) */
class User {
    id: number;
    name: string;

    /** @partialEq({ skip: true }) @hash({ skip: true }) */
    cachedScore: number;
}
After (Generated)
class User {
    id: number;
    name: string;

    cachedScore: number;

    static equals(a: User, b: User): boolean {
        return userEquals(a, b);
    }

    static hashCode(value: User): number {
        return userHashCode(value);
    }
}

export function userEquals(a: User, b: User): boolean {
    if (a === b) return true;
    return a.id === b.id && a.name === b.name;
}

export function userHashCode(value: User): number {
    let hash = 17;
    hash =
        (hash * 31 +
            (Number.isInteger(value.id)
                ? value.id | 0
                : value.id
                      .toString()
                      .split('')
                      .reduce((h, c) => (h * 31 + c.charCodeAt(0)) | 0, 0))) |
        0;
    hash =
        (hash * 31 +
            (value.name ?? '').split('').reduce((h, c) => (h * 31 + c.charCodeAt(0)) | 0, 0)) |
        0;
    return hash;
}

Generated output:

class User {
    id: number;
    name: string;

    cachedScore: number;

    static equals(a: User, b: User): boolean {
        return userEquals(a, b);
    }

    static hashCode(value: User): number {
        return userHashCode(value);
    }
}

export function userEquals(a: User, b: User): boolean {
    if (a === b) return true;
    return a.id === b.id && a.name === b.name;
}

export function userHashCode(value: User): number {
    let hash = 17;
    hash =
        (hash * 31 +
            (Number.isInteger(value.id)
                ? value.id | 0
                : value.id
                      .toString()
                      .split('')
                      .reduce((h, c) => (h * 31 + c.charCodeAt(0)) | 0, 0))) |
        0;
    hash =
        (hash * 31 +
            (value.name ?? '').split('').reduce((h, c) => (h * 31 + c.charCodeAt(0)) | 0, 0)) |
        0;
    return hash;
}