Haxe Abstracts: Wrapping Unsafe Values Without Runtime Overhead
Haxe abstract types let you wrap primitives with domain-specific rules at compile time. Learn how @:forward, operator overloading, and implicit casts improve type safety across targets.
16 Jul 2026, 20:10 UTC

The problem: a primitive is just a number or a string
You have a function sendEmail(address:String). Another part of the codebase passes a username, a URL, or an empty string. The compiler cannot tell the difference because they are all String. The same happens with Float values that are sometimes pixels and sometimes seconds. You end up writing runtime checks that duplicate across the codebase, or worse, you forget one.
The useful takeaway: Haxe abstract types let you give a primitive a name and a set of rules that are checked at compile time, without introducing a wrapper object on every static target.
What an abstract is (and is not)
An abstract is a compile-time construct. It defines a new type over an existing representation type, but it does not create a runtime class. On static targets such as C++ or the JVM, the compiler usually erases the abstract and uses the underlying type directly. That means no allocation, no boxing, and no runtime type check unless you explicitly write one.
This is different from a class wrapper or a typedef. A typedef is just an alias; an abstract adds behavior and restrictions.
Worked example: NonEmptyString
Suppose you need to pass around strings that are guaranteed to be non-empty. You can define an abstract over String:
abstract NonEmptyString(String) {
public function new(value:String) {
if (value == null || value.length == 0)
throw "Value must not be empty";
this = value;
}
@:from static function fromString(value:String):NonEmptyString {
return new NonEmptyString(value);
}
@:to public function toString():String {
return this;
}
@:op(A + B) static function append(a:NonEmptyString, b:NonEmptyString):NonEmptyString {
return new NonEmptyString(a.toString() + b.toString());
}
@:forward(length, charAt, indexOf)
}
The constructor enforces the invariant: if the string is null or empty, construction fails. Because the abstract is erased, a NonEmptyString variable on a static target is just a String in the generated code.
@:forward(length, charAt, indexOf) exposes only those three String methods. Methods that could mutate or obscure the invariant, such as substr or toLowerCase, are not forwarded and therefore unavailable on the abstract type.
The @:from function allows implicit conversion from String to NonEmptyString. The @:to function allows implicit conversion back. This gives you controlled boundaries: you can accept a plain String at an API edge, validate it once, and then work with the abstract internally.
The @:op(A + B) overload lets you concatenate two non-empty strings using the normal + operator, while the constructor guarantees the result is also non-empty.
Target behavior and trade-offs
Abstracts are not uniformly zero-cost across every target. On dynamic targets such as JavaScript, and on some virtual-machine targets like HashLink, the compiler may not fully erase the wrapper. You may see allocation or boxing—wrapping the primitive in an object—unless the abstract is inlined. Performance characteristics vary by target and compiler version, so it is worth checking the generated output rather than assuming.
There is also a compile-time cost. Implicit casts are convenient, but when you chain several @:from conversions the compiler can produce confusing errors if the conversion path is ambiguous. Forwarding a large API surface can also make the abstract feel like a leaky wrapper; it is usually better to forward a small, deliberate set of methods.
| Target family | Typical representation | Runtime cost |
|---|---|---|
| Static (C++, JVM) | Erased to underlying type | Minimal |
| Dynamic or VM (JavaScript, HashLink) | May allocate or box | Possible overhead |
How to check the result
- Write a small test that constructs the abstract, calls a forwarded method, and triggers the constructor failure.
- Compile to at least two targets, for example JavaScript and C++.
- Inspect the generated JavaScript or C++ source to see whether the abstract was inlined or erased.
- If you rely on implicit casts, add a case where conversion should fail and confirm the compiler rejects it.
Start with one invariant that matters for your domain—non-empty identifiers, positive durations, validated emails—and expand only when the rules are stable.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.