Using F# Units of Measure to Catch Unit Errors at Compile Time
Learn how F# Units of Measure add compile‑time dimensional safety to numeric code, see a meter‑to‑feet conversion example, and understand the trade‑offs when interfacing with raw .NET APIs.
05 Feb 2026, 20:28 UTC

Problem: Silent Unit Mistakes in Numeric Code
When you write calculations that involve distances, times, or speeds, it is easy to accidentally treat a value in meters as if it were feet. The mistake compiles and runs, producing wrong results that surface only later in testing or production. The useful takeaway is that F# can make the compiler treat units as part of the type, so such mismatches are caught before the program runs.
How Units of Measure Work
F# lets you define a [Measure] annotation that creates a new dimension. The compiler treats float and float as distinct types, yet at runtime they are both represented as plain float64 values, giving zero‑cost abstraction.
Worked Example: Converting Meters to Feet
// Define measures
[] type m // meter
[] type ft // foot
// Conversion factor: 1 meter = 3.28084 feet
let metersToFeet (x:float) : float =
x * 3.28084
// Example usage
let heightInMeters : float = 5.0
let heightInFeet : float = metersToFeet heightInMeters
// This line would fail to compile:
// let bad = heightInMeters * 2.0 // mismatched units
To see the safety in action, create a console project:
- Run
dotnet new console -lang F# -o UnitDemo - Change directory into
UnitDemoand replace the generatedProgram.fswith the code above. - Execute
dotnet build. The project compiles successfully. - Now edit the file to add the erroneous line
let bad = heightInMeters * 2.0and rundotnet buildagain. The compiler reports an error similar toThe unit of measure 'ft' does not match the unit of measure 'm'.
Because the unit information is erased during compilation, inspecting the emitted IL with a tool like ILSpy shows only ldc.r8 5.0 and mul instructions—no trace of m or ft. This confirms the zero‑cost nature of the feature.
Trade‑offs and Limitations
- Units of Measure apply only to numeric primitives (
float,int,decimal, etc.). You cannot directly annotate a string or a custom class; you must wrap the numeric value in a single‑case discriminated union or a struct if you need richer data. - When calling .NET APIs that expect raw
floatvalues, you must explicitly strip the unit using thefloatfunction (e.g.,let raw = float heightInMeters) or re‑add it after the call. Forgetting this step can re‑introduce unit mismatches at runtime, so establish a convention—such as keeping all internal calculations in measured types and converting only at the system boundary. - The feature relies on compile‑time checking; if you compile with
--optimize-or use dynamic code generation (Reflection.Emit) that bypasses the F# compiler, the safety guarantees disappear.
Actionable Closing
Start by adding measure definitions for the domains you work with (length, time, mass, etc.) and write thin conversion functions as shown. Keep the measured types inside your core library and expose plain float values only at the edges where you interact with other .NET code. This approach gives you compile‑time dimensional safety without any runtime penalty, helping you catch unit‑related bugs early and maintain confidence in numeric code.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.