Using Branded Types to Validate Code Logic
There are a few steps involved in this solution.
Ensure the User is Authorized
Because the user needs to be authorized before they can run a conversion, we'll start by creating an
0:00 Let's start the solution by looking at the AuthorizedUser function. We want to make sure that this performConversion up here, it actually authorizes the user first. Let's create a branded type, so type AuthorizedUser = Brand<User, "AuthorizedUser">. That's cool. 0:22 0: 22 Now instead of this User type on the performConversion, which is the actual root of what's going on, we now accept an AuthorizeUser.
0:31 Yeah, this AuthorizeUser is still actually a user here. We need to make sure that this ensureUserCanConvert now returns an as AuthorizedUser. Hmm, this returned type is a little bit funky there. We can actually change that to AuthorizedUser.
0:51 This should now be passing here, because it should expect an error, because we have an AuthorizedUser before we're performing the conversion. Property brand is missing in type User. Argument of type User is not assignable to parameter of type AuthorizedUser. Perfecto.
1:08 From there then, we can handle the conversion request. We're getting our AuthorizedUser back. That's really good, but actually, we're supposed to be checking on a converted currency, on the converted amount that the user is checking. Let's create that now.
1:25 We have type ConvertedAmount = Brand. It's going to just be a number, because that's the max conversion amount. Yeah, cool. Let's call it ConvertedAmount. Then we have ConvertedAmount, let's stick that into handleConversionRequest...No, sorry. I'm looking at the wrong thing. PerformConversion. This is definitely going to be a ConvertedAmount.
1:52 We also want to make sure that the ensureUserCanConvert, we stick it there as well. Amount is ConvertedAmount. Now, let's see. Yeah, these are erroring properly here, because argument of type number is not assignable to parameter of type ConvertedAmount. That's because our getConversionRateFromApi is returning a Promise<number> instead of a convertedAmount, so let's now add that.
2:18 This one, return Promise.resolve, there are several ways you could annotate this. You could annotate it here, but it actually won't work, because this then will error, because it's not resolving properly. We could return this as Promise, which is a little bit ugly, but we could also stick it inside here. We could say as ConvertedAmount.
2:38 What this does then is it resolves and turns this Promise.resolve into a Promise.ConvertedAmount. That's pretty nice. Now everything is erroring as expected. We have our handleConversionRequest, which takes in the raw values, unvalidated values, converts them, and then this one errors, because this user is not authorized.
3:02 This one down here, if I remove these ts-expect-errors, this one errors because the amount has not been converted yet. God, these lovely error messages. Then finally, you have a ConvertedAmount, AuthorizedUser, and then you can perform the conversion based on those values.
3:16 This is so, so nice. It makes this PR pretty easy to review is you understand these types, because you can just look at this and make sure it's the right type, look at this and make sure it's been authorized, and then ensure that on the type level, the logic is behaving as you expect.
3:36 On an app like this, a financial tech app where you want to make sure the logic is bang on, I couldn't imagine anything better than this.