Type-Safe Currency Conversion with F# Units of Measure

Date: 2024-10-10 | create | tech | fsharp | types |

I'm building Cloud Compare - a site that makes it easy to compare different cloud offerings. I quickly ran into an issue with currency conversion - many clouds price in USD but several (like Hetzner) price in other currencies.

So here I wanted to share an approach to currency conversion that offers a more type-safe experience by leveraging F#'s units of measure feature.

F# Units of Measure

F# has a feature called units of measure. This allows you to add a "unit" to a number value, providing more context to what that number represents. This is useful to you the programmer but it also allows the type system to help you when you make a mistake: "I don't think you can divide float<inches> by float<kg>"

In most languages people build around this by either:

  • Having a separate field to hold the unit - amount and amountType
  • Having a special object that handles this - AmountDto( amount, amountType)

But generally these approaches are all ways to try and add additional info to that base int / float / decimal so we know what it represents.

Common examples:

  • Weight - lbs, g, kg
  • Length - inches, cm, feet
  • Currencies - USD, euro, kronor

Units of measure is just a way to tag this extra info - but do it in such a way that the type system itself can understand it (and thus yell at you at compile time when you do it wrong).

From the MS docs you can define a measure like this:

[<Measure>] type cm

And also define unit conversions like this (here 1 ml == a cubic centimeter cm^3)

[<Measure>] type ml = cm^3

F# Units of Measure for Type-Safe Currency Conversion

For my purposes I want to be able to store all my cloud prices in the currency they're sold in and present them in a currency that makes sense for the user. This makes it easier for me because I can just copy over the price directly (vs trying to convert each time) - and I can render them to the correct price to the end user based on current currency conversion rates.

To do this:

  • Create Measures for usd and euro
  • Create Currency to hold all the currencies I accept
  • Create a currency convertor - here currency -> usd
[<Measure>] type usd 
[<Measure>] type euro

type Currency = 
    | Usd of float<usd> 
    | Euro of float<euro>

let currencyToUsd
    (currency: Currency)
    : float<usd>
    =
    match currency with 
    | Currency.Usd s -> s 
    | Currency.Euro e ->
        // 2024.09.16 - 1 euro : 1.11 dollars 
        (e * 1.11<usd/euro>)

Together this allows me to list prices in their native currency then get type-safe conversion back into USD which is what I display on my site. Plus when I need to update the currency conversions - I can just do that in my central conversion function.

Next

There are probably better ways to do this conversion stuff but I saw an opportunity to try out units of measure and I'm p happy with it so will keep it for now.

If you liked this post you might also like:

Want more like this?

The best / easiest way to support my work is by subscribing for future updates and sharing with your network.