Eliminating Primitive Obsession with Haxe Abstracts
Learn how Haxe Abstracts eliminate primitive obsession by providing compile-time type safety for IDs and value types without the runtime overhead of object instantiation.
23 Aug 2025, 07:21 UTC

The Danger of the Generic Integer
In large-scale applications, it is common to see functions that take multiple integer arguments: processOrder(userId: Int, orderId: Int, storeId: Int). The problem is that to the compiler, these are all just Int. A simple typo—swapping userId and orderId—will not trigger a compile-time error, but it will cause a silent, critical failure in production.
The traditional solution is to wrap these primitives in classes. However, creating a new object instance for every ID in a high-frequency loop introduces memory pressure and garbage collection overhead. This is where Haxe Abstracts provide a middle ground: the type safety of a class with the runtime performance of a primitive.
Zero-Cost Type Wrappers
An Abstract is a Haxe type that allows you to define a high-level API for a type that is erased during compilation. Unlike a class, which exists as an object at runtime, an abstract is a compile-time construct. When you compile Haxe code to JavaScript, C++, or Java, the abstract is replaced by its underlying type (the "underlying type").
This means you can define a UserId type that the compiler treats as distinct from an OrderId, but the target machine only ever sees a raw integer. This pattern eliminates "Primitive Obsession"—the tendency to use basic data types for complex domain concepts—without sacrificing performance.
Implementing a Type-Safe Identifier
To implement an abstract, you define the underlying type and provide a way to instantiate it. Using the inline keyword is critical here; it tells the compiler to replace the method call with the actual logic, ensuring no function call overhead remains in the generated code.
// Define a type-safe wrapper for a User ID
abstract UserId(Int) {
public function new(value: Int) this = value;
// An inline method to provide domain-specific logic
public inline function toString(): String {
return \"user_id: \" + this;
}
}
// Define a separate wrapper for an Order ID
abstract OrderId(Int) {
public function new(value: Int) this = value;
}
class OrderProcessor {
public static function process(u: UserId, o: OrderId): Void {
trace(\"Processing order \" + o + \" for user \" + u);
}
}
// Usage
var myUser = new UserId(123);
var myOrder = new OrderId(456);
// This works:
OrderProcessor.process(myUser, myOrder);
// This will trigger a COMPILE-TIME error:
// OrderProcessor.process(myOrder, myUser);
Verification and Runtime Behavior
To verify that the abstraction is "zero-cost," you can examine the generated output. If compiling to JavaScript, the UserId and OrderId types disappear entirely. The resulting JS will simply pass raw numbers to the function.
How to check:
- Compile the above code using
haxe -js main.js. - Open
main.jsand search for theprocessfunction. - Observe that the arguments are handled as standard numbers, not as object instances.
Trade-offs and Limitations
While powerful, abstracts are not a replacement for classes. Because they are erased at runtime, they have specific limitations:
- No Inheritance: Abstracts cannot extend other types or be extended. They are value-like, not object-like.
- Generic Constraints: You cannot use an abstract as a type parameter in certain generic contexts where the target language requires a real object (e.g., some Java generic collections).
- Code Bloat: Because
inlinemethods copy the logic to every call site, extremely large inline functions used in thousands of places can increase the size of the generated binary or script.
Practical Application
Use abstracts whenever you have a primitive that represents a specific domain concept. Common candidates include: Milliseconds (wrapping Float), CurrencyCode (wrapping String), or DatabaseKey (wrapping Int). By moving the validation of these types from runtime checks to the compiler, you reduce the surface area for bugs while keeping your execution speed at the hardware limit.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.