Derive Macros
Derive macros generate code from type structure, triggered by @derive(MyDerive). The @derive decorator generates typeclass instances that work with summon() and operator overloading.
When to Use
- Generate implementations based on fields
- Create serialization/deserialization
- Build type guards
- Generate builder patterns
Basic Structure
typescript
import { defineDeriveMacro, type DeriveTypeInfo } from "@typesugar/core";
import { quoteStatements } from "@typesugar/core/quote";
defineDeriveMacro("MyDerive", {
expand(ctx, target, typeInfo: DeriveTypeInfo) {
// typeInfo contains:
// - name: string (type name)
// - fields: FieldInfo[] (properties)
// - isClass: boolean
// - isInterface: boolean
// - variants?: VariantInfo[] (for unions)
return quoteStatements(ctx)`
${target}
// Generated code here
`;
},
});DeriveTypeInfo Structure
typescript
interface DeriveTypeInfo {
name: string; // "User"
fields: FieldInfo[]; // Properties
isClass: boolean;
isInterface: boolean;
typeParameters?: TypeParam[]; // Generic params
variants?: VariantInfo[]; // For discriminated unions
}
interface FieldInfo {
name: string; // "email"
type: ts.Type; // TypeScript type
typeString: string; // "string"
optional: boolean;
readonly: boolean;
}Tutorial: Creating @derive(Printable)
typescript
import { defineDeriveMacro } from "@typesugar/core";
import { quoteStatements, ident } from "@typesugar/core/quote";
defineDeriveMacro("Printable", {
expand(ctx, target, typeInfo) {
const { name, fields } = typeInfo;
// Build field printing
const fieldPrints = fields.map((f) => ` ${f.name}: \${this.${f.name}}`).join(",\\n");
return quoteStatements(ctx)`
${target}
${ident(name)}.prototype.print = function(): string {
return \`${name} {
${fieldPrints}
}\`;
};
`;
},
});Usage:
typescript
@derive(Printable)
class User {
constructor(
public name: string,
public age: number
) {}
}
new User("Alice", 30).print();
// User {
// name: Alice,
// age: 30
// }Simplified Derive API
For simple derives, use the simplified API:
typescript
import { defineCustomDerive } from "@typesugar/core";
// String-based (returns code as string)
defineCustomDerive("Simple", (typeInfo) => {
return `
${typeInfo.name}.prototype.simplify = function() {
return { type: "${typeInfo.name}" };
};
`;
});Or for field-level derives:
typescript
import { defineFieldDerive } from "@typesugar/core";
defineFieldDerive("Validate", (field) => {
if (field.typeString === "string") {
return `if (typeof value.${field.name} !== "string") throw new Error("Invalid ${field.name}");`;
}
return "";
});Handling Different Field Types
typescript
defineDeriveMacro("Clone", {
expand(ctx, target, typeInfo) {
const cloneExprs = typeInfo.fields
.map((f) => {
if (f.typeString.startsWith("Array<")) {
return `${f.name}: [...this.${f.name}]`;
} else if (f.typeString === "Date") {
return `${f.name}: new Date(this.${f.name})`;
} else if (f.typeString.includes("{")) {
// Object type
return `${f.name}: { ...this.${f.name} }`;
} else {
// Primitive
return `${f.name}: this.${f.name}`;
}
})
.join(", ");
return quoteStatements(ctx)`
${target}
${ident(typeInfo.name)}.prototype.clone = function(): ${ident(typeInfo.name)} {
return new ${ident(typeInfo.name)}(${cloneExprs});
};
`;
},
});Sum Types (Discriminated Unions)
Handle discriminated unions:
typescript
defineDeriveMacro("Match", {
expand(ctx, target, typeInfo) {
if (!typeInfo.variants) {
ctx.reportError(target, "Match requires a discriminated union");
return target;
}
const cases = typeInfo.variants
.map(
(v) => `
${v.discriminant}: (value: ${v.name}) => R
`
)
.join(",\n");
return quoteStatements(ctx)`
${target}
type ${ident(typeInfo.name)}Matcher<R> = {
${cases}
};
function match${ident(typeInfo.name)}<R>(
value: ${ident(typeInfo.name)},
matcher: ${ident(typeInfo.name)}Matcher<R>
): R {
return matcher[value.tag](value as any);
}
`;
},
});Generic Types
Handle generic type parameters:
typescript
defineDeriveMacro("Functor", {
expand(ctx, target, typeInfo) {
if (!typeInfo.typeParameters?.length) {
ctx.reportError(target, "Functor requires a type parameter");
return target;
}
const typeParam = typeInfo.typeParameters[0].name;
return quoteStatements(ctx)`
${target}
${ident(typeInfo.name)}.prototype.map = function<B>(
f: (a: ${ident(typeParam)}) => B
): ${ident(typeInfo.name)}<B> {
// Implementation...
};
`;
},
});Dependencies Between Derives
Specify that one derive requires another:
typescript
defineDeriveMacro("Ord", {
requires: ["Eq"], // Must derive Eq first
expand(ctx, target, typeInfo) {
// Can assume equals() exists
return quoteStatements(ctx)`
${target}
${ident(typeInfo.name)}.prototype.compare = function(other: ${ident(typeInfo.name)}): number {
// Implementation using equals()...
};
`;
},
});Static Methods
Add static methods:
typescript
defineDeriveMacro("Default", {
expand(ctx, target, typeInfo) {
const defaults = typeInfo.fields
.map((f) => {
switch (f.typeString) {
case "number":
return "0";
case "string":
return '""';
case "boolean":
return "false";
default:
return "undefined as any";
}
})
.join(", ");
return quoteStatements(ctx)`
${target}
${ident(typeInfo.name)}.default = function(): ${ident(typeInfo.name)} {
return new ${ident(typeInfo.name)}(${defaults});
};
`;
},
});Testing Derives
typescript
import { expandCode } from "@typesugar/testing";
describe("Printable derive", () => {
it("generates print method", async () => {
const result = await expandCode(`
import { derive } from "@typesugar/derive";
@derive(Printable)
class Point {
constructor(public x: number, public y: number) {}
}
`);
expect(result.code).toContain("Point.prototype.print");
expect(result.code).toContain("x: ${this.x}");
});
});Best Practices
- Handle all field types: Primitives, objects, arrays, dates
- Support optional fields: Check
field.optional - Handle generics: Use type parameters correctly
- Declare dependencies: Use
requiresfor derive ordering - Generate type-safe code: Include proper type annotations
Next Steps
- Expression Macros — Function call macros
- Testing Macros — Verifying macro output
- Publishing Macros — Distributing derives
