Haxe Conditional Compilation As Platform Seams, Not Sprawl
Use Haxe conditional compilation as a compile-time seam for platform differences, not as general branching. Isolate JS and C++ APIs behind small abstractions, keep flags in one place, and verify generated output is stripped per target.
23 Feb 2026, 20:58 UTC

The concrete problem is familiar to anyone shipping Haxe to both browser and native: you need the DOM in JavaScript and the file system in C++, from the same source tree, without shipping dead code or runtime feature checks on every call. The useful takeaway is to treat Haxe conditional compilation as a compile-time seam for platform differences, not as a general branching mechanism.
Where platform differences leak
Haxe compiles to JavaScript, C++, HashLink, Java, and others. The type system is shared, but the runtime APIs are not. A logger that writes to console in the browser and to a file on desktop cannot be expressed with a single portable call.
Conditional compilation lets the compiler choose code at build time. Blocks guarded by #if are evaluated by the compiler, and the non-matching branches are removed from the generated output for that target. That means no runtime cost and no dead code shipped.
Flags come from the target itself, such as js, cpp, hl, and from custom defines passed with -D on the command line or in an .hxml file. Because evaluation is compile-time, the compiler also type-checks only the active branch for the current target.
Keeping the seams small
The risk is readability. Scattering #if throughout business logic creates spaghetti code and hides code paths that are never type-checked for a given target. A practical pattern is to isolate platform-specific calls behind a tiny, well-named interface and keep the conditionals in one place.
Combining conditional compilation with Abstract types is a common way to get a zero-cost abstraction that behaves differently per target. The abstract defines a single public API, and each implementation is compiled only for its target.
Worked example: platform time source
Main.hx
class Main {
static function main() {
trace(Platform.now());
}
}
abstract PlatformTime(Float) from Float to Float {
@:from static function fromFloat(v:Float):PlatformTime return v;
@:to static function toFloat(v:PlatformTime):Float return v;
public static function now():PlatformTime {
#if js
return untyped __js__('Date.now()');
#elseif cpp
return cpp.vm.Time.now();
#else
return 0.0;
#end
}
}
Build files keep the defines explicit.
# js.hxml
-js main.js
-main Main
-D js# cpp.hxml
-cpp bin
-main MainWhen compiling with haxe js.hxml the compiler sees the js flag true and cpp false. The #if js branch is kept, the #elseif cpp branch is stripped. The opposite happens for the C++ build. Inspecting the generated JavaScript or C++ source shows the non-matching block is absent.
An alternative without Abstract is a plain module with a single conditional function:
class Platform {
public static function now():Float {
#if js
return untyped __js__('Date.now()');
#elseif cpp
return cpp.vm.Time.now();
#else
return 0.0;
#end
}
}
Callers use Platform.now() and never see the flags.
Trade-offs and how to verify
Overuse of conditional blocks makes code hard to read and review. Logic hidden behind flags is not type-checked for targets where the flag is false, so a typo in a disabled branch can surface only when that target is built. Testing multiplies with each flag permutation.
A practical way to check the result is to compile for two targets and search the generated output for strings that should only exist in one branch. If the block was stripped correctly, the string will be absent. Keeping platform modules small also makes it easier to run a build matrix for each target in CI.
Use conditional compilation for narrow, well-documented seams, centralize it behind abstractions, and keep the rest of the codebase target-agnostic.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.