Haxe Type Safety: Choosing Between Abstract Types and Classes
Learn when to use Haxe Abstracts versus Classes to eliminate primitive obsession and reduce runtime memory overhead in domain-driven designs.
09 Apr 2026, 21:28 UTC

The Problem: Primitive Obsession vs. Runtime Overhead
In domain-driven design, using raw primitives (like Int for a User ID or String for an Email) leads to "primitive obsession." This occurs when a function signature like function updateRecord(id:Int, accountId:Int) allows a developer to accidentally swap the two arguments without triggering a compiler error.
The standard solution is to wrap these primitives in a Class. However, in performance-critical applications or when handling thousands of small objects, the memory overhead of object instantiation and garbage collection for every single ID or coordinate can degrade performance. The goal is to achieve strict type safety without paying a runtime memory penalty.
Abstracts vs. Classes: Decision Matrix
Haxe provides abstract types specifically to bridge the gap between the safety of a class and the performance of a primitive.
| Feature | Abstract Type | Class Type |
|---|---|---|
| Runtime Overhead | Zero (compiled to underlying type) | Object instantiation overhead |
| Inheritance | None (cannot extend or be extended) | Full OO inheritance support |
| Internal State | Single underlying value only | Multiple fields and mutable state |
| Type Safety | Strong (prevents primitive mixing) | Strong (full object typing) |
| Method Execution | Usually inlined by compiler | Standard virtual method dispatch |
Engineering Trade-offs
When to use Abstracts
Use abstract when you are creating Value Objects. A Value Object is defined by its attributes rather than a unique identity. Examples include currency amounts, distance measurements, or unique identifiers. Because Haxe inlines abstract methods, the generated code for a target like JavaScript or C++ will treat the abstract as if you had used the raw primitive, but the Haxe compiler will block any attempt to pass a raw Int where a UserId abstract is expected.
When to use Classes
Use class when you need Entity Objects. If the object needs to maintain a complex internal state (multiple fields), participate in a polymorphic hierarchy (inheritance), or be passed to an external API that expects a specific object structure, a class is required. Abstracts are limited to wrapping a single underlying type.
The Inlining Risk
While the inline keyword in abstract methods eliminates function call overhead, overusing it for massive, complex logic blocks can lead to "code bloat." The compiler replaces every call site with the actual method body, which can increase the size of the final binary or script.
Implementation Example: Type-Safe Identifiers
The following example demonstrates how to prevent the accidental swapping of two different ID types using abstracts.
// Define abstracts to wrap the underlying Int type
abstract UserId(Int) {}
abstract AccountId(Int) {}
class AccountManager {
// This signature prevents passing a UserId into the accountId slot
public function linkUserToAccount(user:UserId, account:AccountId):Void {
trace("Linking user " + user + " to account " + account);
}
}
class Main {
static function main() {
var u = new UserId(101);
var a = new AccountId(5005);
var manager = new AccountManager();
// Correct usage
manager.linkUserToAccount(u, a);
// COMPILER ERROR: This will fail because AccountId is not UserId
// manager.linkUserToAccount(a, u);
// COMPILER ERROR: This will fail because Int is not UserId
// manager.linkUserToAccount(101, a);
}
}
Verification and Validation
To verify that the abstract is providing zero-cost abstraction, follow these steps:
- Compile for JS: Run
haxe -main Main -js main.js. - Inspect Output: Open
main.jsand search for thelinkUserToAccountcall. You will observe that the variables passed are raw numbers, not object instances. - Type Check: Attempt to pass a raw integer to the function in your
.hxfile. The Haxe compiler should throw aType mismatcherror during the build phase.
Rollback and Migration
If you discover that an abstract needs to support multiple fields or inheritance, you must convert it to a class. This is a breaking change: you will need to replace all direct instantiation (e.g., new UserId(1)) with class constructor calls and update any code that relied on the abstract's implicit casting to the underlying type using explicit casting (cast user as Int).
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.