Zero‑Cost Wrappers in Haxe Using Abstract Types
Learn how Haxe abstract types give you strong typing for primitives like metres or seconds without any runtime overhead, with a worked example and verification steps.
28 Oct 2025, 06:48 UTC

Problem: Adding units without runtime cost
\nWhen you write cross‑platform code you often want to give primitive values a domain meaning—metres, seconds, pixel counts—so the compiler can catch mistakes like adding a length to a time. Wrapping a Float in a class works, but it creates objects at runtime, adds allocation overhead, and prevents inlining on some targets. Haxe’s abstract type solves this by letting you define a wrapper that disappears after compilation, giving you strong typing with zero‑cost abstraction on JavaScript, C++, Java, and all other supported platforms.
Thesis
\nHaxe abstract types let you define lightweight wrappers over existing primitives. The wrapper is erased during code generation, so the emitted code uses the underlying representation directly. You get compile‑time safety and API ergonomics without paying any runtime price.
\nDefining an abstract type
\nAn abstract is declared with the abstract keyword, an underlying representation, and optional inline functions that operate on it. Because the functions are inline, the compiler substitutes their bodies at the call site, and the abstract type itself is not emitted.
abstract Meter (Float) from Float to Float {\n public inline function new(v:Float) this = v;\n /** Add two Meter values */\n public inline function +(other:Meter):Meter return this + other;\n /** Convert metres to kilometres */\n public inline function toKm():Float return this / 1000.0;\n}\n\nThe from and to clauses enable implicit conversion to and from the underlying Float when needed, but you can omit them to keep the wrapper strictly opaque.
Worked example: preventing mixed units
\nSuppose you have two abstractions, Meter and Second, both based on Float. Adding them should be a compile‑time error.
abstract Meter (Float) from Float to Float { public inline function new(v:Float) this = v; }\nabstract Second (Float) from Float to Float { public inline function new(v:Float) this = v; }\n\nclass Main {\n static function main() {\n var d:Meter = new Meter(5.0);\n var t:Second = new Second(2.0);\n // This line will not compile:\n // var bad = d + t;\n // Correct usage:\n var dist:Meter = d + new Meter(3.0);\n var time:Second = t + new Second(1.0);\n trace(dist); // 8\n trace(time); // 3\n }\n}\n\nTo verify that no wrapper objects appear in the output, compile for JavaScript and inspect the generated file.
\n- \n
- Save the code above to
src/Main.hx. \li>Run the Haxe compiler (you need Haxe 4.x installed):\n - Open
out.jsand search for the variable names. You will see something like:\n
\nvar d = 5.0;\nvar t = 2.0;\nvar dist = d + 3.0;\nvar time = t + 1.0;\nconsole.log(dist);\nconsole.log(time);\nThe
\nMeterandSecondtypes are gone; only rawNumbervalues remain, confirming zero‑cost abstraction. \n - Repeat for C++ to see the same effect:\n
\nhaxe -main Main -cpp out\nIn
\nout/Main.cppyou will find the operations performed ondoublevariables, with no class definitions forMeterorSecond.
haxe -main Main -js out.js\n\nTrade‑off / limitation
\nBecause abstracts are erased, debugging can be harder: the wrapper type does not appear in stack traces or generated source, so you see only the underlying Float or double. Also, runtime type information (RTTI) cannot distinguish Meter from a plain Float, which may affect libraries that rely on class names for serialization or reflection. If you need the wrapper to be visible at runtime (e.g., for polymorphic behaviour), you must use a regular class instead of an abstract.
A practical way to check that the abstraction truly erased is to build with full dead‑code elimination (-dce full) and compare the size of the output with and without the abstract. If the size does not grow, the wrapper contributed no runtime overhead.
Actionable closing
\nPick a primitive that appears frequently in your codebase—coordinates, timestamps, or identifiers—and wrap it in an abstract. Define inline functions for the operations you need, compile with -dce full, and inspect the generated JavaScript or C++ to confirm the wrapper vanished. Over time you can enrich the API with more inline methods, safe in the knowledge that the abstraction remains zero‑cost across all Haxe targets.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.