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.

Foreign Types

A foreign type is one you don’t own and can’t annotate: DateTime from effect, Decimal from a math library, and so on. Instead of adding encodeWith to every field that uses it, configure it once in macroforge.config.ts and every derive picks it up.

// macroforge.config.ts
import { DateTime } from "effect";

export default {
  foreignTypes: {
    "DateTime.Utc": {
      from: ["effect"],
      encode: (v) => DateTime.formatIso(v),
      decode: (raw) => DateTime.unsafeFromDate(new Date(raw)),
      default: () => DateTime.unsafeNow(),
      hasShape: (v) => typeof v === "string"
    }
  }
};

Now any field typed DateTime.Utc encodes through your handlers.

Options

KeyPurpose
fromModule paths this type may be imported from
aliasesAdditional { name, from } pairs naming the same type
encodeValue → JSON-safe representation
decodeRaw JSON → value
defaultValue used by @derive(Default) and by missing-field defaults
hasShapeWhether a value is this type, raw or decoded: tells it apart from the other members of a union

In a union with your own types

A field can hold a foreign type or one of your own, as RecordId | Employee does, often through a generic alias such as type RecordLink<T> = /** @default */ RecordId | T declared in another module. Decoding asks hasShape of the raw value and encoding asks it of the live one, so it must recognise both:

hasShape: (v): v is RecordId | string => v instanceof RecordId || typeof v === "string"

Whatever hasShape accepts goes through the foreign handlers; everything else goes through your type’s own encoder and decoder. Expansion fails when such a union needs a handler the config does not give.

Import-source validation

from exists to disambiguate types that share a name across packages. A Decimal from decimal.js and one from your own module are different types that happen to collide.

When a type’s name matches a foreign-type entry but its import source doesn’t, the match falls through to generic handling silently: the type may be a different one, and TypeScript reports any mismatch. If a foreign type appears not to be applying, check that the module path in from matches how the file actually imports it.

When the name and source match but the qualification doesn’t, such as DateTime for "DateTime.DateTime", expansion warns and names the alias that would match it.

Namespaced names

A dotted key like "DateTime.Utc" is interpreted as namespace DateTime, type Utc. The namespace is derived from the key: there’s no separate namespace option to set. To match the type under another name or path, list it in aliases.

Setup

Generated code imports your handlers from #macroforge/config, which your manifest has to declare once:

macroforge init

Expansion fails with an error naming the missing entry until it is declared. Expanded Config shows what the handlers become and why, with generated code before and after.