Swift: Money with Phantom Types 👻

When I wrote about enums with no cases yesterday, both @mbrandonw and @jl_hfl pointed me to Phantom Types 👻!

I’ve seen Phantom Types before, including in this objc.io article about Phantom Types and in conference talks, but while I like the idea of Phantom Types and think they’re cool and interesting, I haven’t actually used them ever in my own code. Mostly, because it’s not yet natural for me to see a problem and think “Ah, Phantom Types would be the perfect solution!”.

However, I would like to have Phantom Types more accessible for myself in the future and I really enjoyed the example that @johannesweiss gave in his talk The Type System is Your Friend, so I’m going to write his example out here to be more searchable / discoverable for myself and hopefully others!

The problem that @johannesweiss with Phantom Types is currency conversion 💸🤑💸!

While in the video, @johannesweiss goes straight into the Phantom Type solution, I wanted to first try a solution without the Phantom Types to see the before and after!

Without Phantom Types 😱

I would probably have a Money struct that includes both the amount and the currency of the amount:

But let’s say I want to convert the money to a different currency – let’s say from GBP to EUR:

We can immediately see the problem. I have to check to make sure that the money passed into my function is of the correct currency. And if it’s not, I have to trhow an Error. Which cascades down and now the function that calls this function has to handle an error case!

With Phantom Types 👻

Phantom Types are Types that don’t have any functionality. According to @johannesweiss:

A phantom type is a parameterized type whose type parameters do not all appear in its definition. I have what you could call a useless type parameter C. This is the Currency, and it is not even used in the actual properties of the struct. To fill Money, I created an empty protocol, Currency, so that I may mark any type as a currency.

Like this:

Now the GBP -> EUR conversion function looks like this!

Notice that we no longer need to manually check for correct currency type or return any errors. The compiler does the work for us!

Update

The point of this blog post is to illustrate the basics of using Phantom Types. However, as @oisdk points out, you can go further and use Phantom Types to automatically pick the correct conversion:

@davedelong further optimized the solution to this:

Enjoy the article? Join over 17,500+ Swift developers and enthusiasts who get my weekly updates.

  • Abdullah

    great post! I have question, in convertGBPtoEUR func what dose this – & g t mean?

    • Abdullah

      oh… html entity for “>”

      • Yeah sorry, sometimes the HTML gets messed up. Fixed it!

        • Stuart Kent

          Are the generic types being swallowed here, or am I missing something else?

          • Apokrupto

            I think it’s an editing issue, the code as it is compiles, even badMoney in the example. All the generics have been removed accidentally

          • Yeah – sorry. Things got messed up somehow. Updated!

          • Stuart Kent

            No problem, and thanks for the article!

  • Hugh Berney

    nice article.

    However I think you may have a typo in the following paragraph:


    // the method signature is super nice now!
    // inputing Money in GBP, return Money in EUR!
    func convertGBPtoEUR(gbp: Money) -> Money {
    let forex = NSDecimalNumber(mantissa: 133263, exponent: -5, isNegative: false)
    let convertedAmount = gbp.amount.decimalNumberByMultiplyingBy(forex)
    return Money(amount: convertedAmount)
    }

    // Usage
    let fivePounds = Money(amount: 5)
    let threeEuros = Money(amount: 3)
    let convertedMoney = convertGBPtoEUR(fivePounds) // success!
    let badMoney = convertGBPtoEUR(threeEuros) // compiler error!

    Shouldn’t it be:

    // the method signature is super nice now!
    // inputing Money in GBP, return Money in EUR!
    func convertGBPtoEUR(gbp: GBP) -> EUR {
    let forex = NSDecimalNumber(mantissa: 133263, exponent: -5, isNegative: false)
    let convertedAmount = gbp.amount.decimalNumberByMultiplyingBy(forex)
    return EUR(amount: convertedAmount)
    }

    // Usage
    let fivePounds = GBP(amount: 5)
    let threeEuros = EUR(amount: 3)
    let convertedMoney = convertGBPtoEUR(fivePounds) // success!
    let badMoney = convertGBPtoEUR(threeEuros) // compiler error!

    You’re still using the type Money and not the fantom type EUR && GBP ?

    ++

    • Hmmm.. must have gotten messed up somehow with the HTML / Update. Fixed it!

      • Hugh Berney

        👍👍👍

  • so this phantom type thing is like a wild card right?

    • hamsternik

      It’s more convenient to look up phantom types as like a marker interface pattern which works with genercis