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.

Newtype

$Newtype<T> is a type-position macro that gives T a nominal brand. Two newtypes over the same base are distinct types, and neither accepts a bare T without going through a constructor or guard.

Generated Output

export type Meters = $Newtype<number>;

expands to

export type Meters = number & { readonly [__mf_newtype_42]: true };
declare const __mf_newtype_42: unique symbol;

Every expansion declares its own unique symbol, named from the expansion’s id, so the brand needs no name and cannot collide, even for a $Newtype that another macro emits. The declare const is type-only and erases at runtime. A base with a top-level | or &, or a function or conditional type, is parenthesized.

$Newtype is built in and needs no import. It nests, so $Newtype<$Newtype<number>> carries both brands.

The brand does not involve the alias’s own type parameters: with type Id<T> = $Newtype<string>, Id<User> and Id<Order> are the same type. A brand that should differ per parameter names it in the type, as in type Id<T> = string & { readonly [IdOf]: T } with a declare const IdOf: unique symbol.

Constructing Values

A newtype has no constructor of its own. Derive Decode on the alias: for a primitive base, decode checks the typeof and runs any validators written on the alias, which makes it the validated constructor, and is the narrowing guard.

/** @derive(Encode, Decode) */
/** @endec(nonNegative, finite) */
export type Meters = $Newtype<number>;

const parsed = Meters.decode(input);   // { success, value } | { success, errors }
if (Meters.is(distance)) {
    travel(distance);                   // narrowed to Meters
}

Encode writes the bare primitive, and Default returns the primitive’s default (or the alias’s @default(value)) cast to the newtype.

Error Handling

The macro reports an expansion error when it does not receive exactly one type argument.