Haxe Abstract Types: Zero-Cost Domain Primitives
Use Haxe abstract types to catch mixed-up IDs and units at compile time without runtime allocation.
07 Jun 2026, 00:38 UTC

The problem: plain integers let domain values mix
If user IDs, order IDs, and currency amounts are all typed as Int, the compiler cannot stop you from passing an order ID to a function that expects a user ID, or adding two monetary values in different currencies. These mistakes compile cleanly and fail later, often with unclear stack traces.
Useful takeaway
Haxe abstract types create distinct compile-time names for an existing underlying type. The compiler treats UserId and OrderId as unrelated even when both wrap Int, then erases the wrapper during code generation. The target runtime sees only the raw integer or float, so there is no object allocation and no runtime indirection.
Worked example: nominal IDs
Save this as Domain.hx:
abstract UserId(Int) {
public inline function new(id:Int) this = id;
public inline function toString():String return "User(" + this + ")";
}
abstract OrderId(Int) {
public inline function new(id:Int) this = id;
public inline function toString():String return "Order(" + this + ")";
}
class Main {
static function ship(order:OrderId, buyer:UserId):Void {
trace("Shipping " + order + " to " + buyer);
}
static function main():Void {
var buyer = UserId(42);
var order = OrderId(7);
// Compiler error: OrderId cannot be used where UserId is expected
// ship(order, buyer);
ship(order, buyer);
}
}
Both UserId and OrderId wrap Int, but the declaration creates nominal types. The commented ship(order, buyer) call swaps the arguments and produces a compile error because OrderId is not a subtype of UserId.
Compile and verify erasure
Run the Haxe compiler from your project directory. No special permissions are required. Replace Main with your entry class and build/out.js with your output path:
haxe -main Main -js build/out.js
Open build/out.js and search for UserId or OrderId. You should find no wrapper classes or constructor calls; the generated code uses plain numbers. This confirms zero-cost erasure.
To confirm the compiler catches mistakes, uncomment the swapped ship(order, buyer) line and run:
haxe -main Main --no-output
The compiler should report a type error. --no-output compiles without writing files, which is useful for quick type checks.
Opt-in conversions
Abstracts do not implicitly convert to or from their underlying type unless you ask. Add from and to to the declaration to allow implicit conversion in one or both directions:
abstract Money(Float) from Float, Float {
public inline function new(amount:Float) this = amount;
public inline function add(other:Money):Money return Money(this + other);
public inline function toString():String return "$" + this;
}
Here from Float lets you write Money(10.0) or, in some contexts, assign a Float directly. to Float lets a Money value flow into a Float parameter. Omit both when you want strict separation, as with the ID example above.
Limits and common mistakes
- Not subtypes. Without
to Int, aUserIdcannot be assigned to anIntvariable. This is intentional; add conversion metadata only when the API genuinely needs it. - Runtime reflection sees the wrapper's contents. Serialization,
typeof, and debug output show the underlyingIntorFloat, notUserId. Libraries that inspect types will not know about the abstract wrapper. - No inheritance. Abstract types cannot be extended or used as base classes.
- Implicit conversions obscure data flow. Heavy use of
fromandtocan hide where a raw value enters a domain type, weakening the compile-time guarantee you added the abstract to enforce.
Practical check
- Write a module with two abstracts over
Intand a function that accepts only one. - Attempt to pass the other abstract and confirm the compiler rejects it.
- Compile to JavaScript or another target and inspect the output for raw underlying values instead of abstract wrapper objects.
- If you add
fromorto, verify that the implicit conversion behaves as expected at the call site, not just in the declaration.
Abstract types are a compile-time mechanism supported across Haxe targets because the compiler replaces them with the underlying representation during code generation. Use them for domain primitives where mixing values must be caught before the program runs.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.