Opaque / Branded Types in Typescript
Tags: typescript, webdev, programming
Published: 2022-09-28 15:12:53

Typescript has amassed quite the following as one of the top ten most popular programming languages over the last several years. And for good reason! It has greatly accelerated development by providing compile-time guarantees that supports a greater development velocity while maintaining a high bar of quality.

As a quick overview, Typescript provides compile-time type checking in order to prevent common datatype bugs (among other things 😉). It has several native types defined that you can annotate variables with. These range from `number`, to `string`, to array/tuple definitions, and more. It even supports more complex types with unions, intersections, and string literals that can allow for incredibly expressive types to reflect the data that you are working with.

However, `number` or `string` isn't always specific enough. Sometimes our code specifically needs a value between 0-1 or only a valid email should be used.

# What's wrong with just using the general type?

Although more general types like `number` or `string` can suffice in terms of general compile-time checks, they fail to provide checks for more nuanced cases. Take for example a function that requires a percentage as an input:

```tsx
/**
* Sets the alpha value for an image.
* @param percentage - a number between 0 and 1
*/
function setAlpha(percentage: number) { ... }
```

In this case, we want a `number` type passed in, but specifically a number between 0 and 1. However, this can cause several potential problems:

```tsx
setAlpha(0.5) // Correct
setAlpha(-0.5) // Runtime Error: Invalid Percentage - Typescript doesn't catch this
setAlpha(2.3) // Runtime Error: Invalid Percentage - Typescript doesn't catch this
setAlpha('abc') // Build Error: string instead of number - Typescript does catch this
```

In classical Javascript fashion, one would usually say "add a runtime check." We could do that, but before Typescript, we would also create runtime checks to confirm that data was a `number` in the first place.

What if we could do the same for this case? It would make sense to try to add type checking for percentages as well since that would catch the types of errors mentioned above during compile-time.

Ideally, we want to just specify that our function only accepts a percentage as its input:

```tsx
function setAlpha(percentage: Percentage) { ... }
```

But how do we create a `Percentage` type?

# Defining a New Type

One may think of aliasing as creating a new type (i.e. `type Percentage = number`), but that merely gives a new name to an existing type. The problem with an aliased type is that it's purely descriptive rather than a functional change (i.e. it's interchangeable with the original type).

```tsx
const value: number = 1.5 // Invalid `Percentage`
setAlpha(value) // Works even though setAlpha expects `Percentage`
```

What we want to do is to create a new opaque type (also known as a tagged / branded type). Other type systems, like Flow, [already have this ability built in](https://flow.org/en/docs/types/opaque-types/), but Typescript does not.

Instead, to achieve something similar, we can 'tag' the type to indicate that it's different from the base type (in this case a `number`):

```tsx
type Percentage = number & { _tag: 'percentage' }
```

By intersecting `number` with a unique object, we prevent the type that we have defined from accepting any value that satisfies the base type. In this case, a generic `number` would not be accepted in functions that expect a `Percentage`:

```tsx
const value: number = 0.5 // Valid `Percentage`, but typed as a general `number`
setAlpha(value) // Build Error: number instead of Percentage
```

[_Typescript playground example_](https://www.typescriptlang.org/play?#code/C4TwDgpgBAChBOBjCA7YBDA5hAjFAvFCgK4C2ARggLABQoksCyaWEATAUWZfFAGRQA3lAD6GTAC4oAIkhJU4iNKgBfWrQBmxFImABLAPYooAZwjAAggBswAC3QAGABRypJCggCUQqAHoAVAB0wf6+qprauobGZpY29jguCFJw8izYON7CAcGBoeE0Wjr6Rqbm1nbobEnwKUwKrGxZfkEhYWo0tIhGJsBQAG7oVg5u3AicDoEArF09fYNWOHVpiniEkzM0vmEA6tAoEBAAJlDABlDd9ae20PTQJno60AAGG89Q5CBQRxAa6MRWPp6EyncDHKDoEHPdw8Z6zFC9AZDDjraYQkGpZiKNjqLZhaxWUGQEEGDRQZ4Ld7oeDQdCIZBgYDgh5Pa4QEAAchpEKshPgBgMTJOjzZUDA1PQpA5ILu5JhCDhNFiFXszgWDk8tGV8UcTgWmS15R1auRms6eKgBKJEBJZIpQyp3LpDKFUA0Bl4wBupkl0BpkNKkPJ2sqDneLOQPL0kJtEO5wH9rqsegA1rdvR69JhHkNrYa4pVEuqzSGEnqhgalUbC+WrE1cdsoAAhQW2Wzk9VUlAne2LR23RPgoPQsbwd4Ad1sekQ7eBUEe3VI4v05Cs0HHei9otIHvukEQeg00-JmIa2DYitLVVrGr8YQAovB+bwALRcDy8OcoQXoh6YFDoKutznKe6QQPmKrXvq3iNo+z5QG+8qfiC359JCf4AUBpwgfUYEQTq1QLE0QA)

However, you may now wonder how do you define a variable as a `Percentage` type. The simple method is to explicitly cast the value with the `as` keyword:

```tsx
const value = 0.5 as Percentage
```

The downside is that this is only reasonable for constants specified at compile-time, what about runtime values?

## Conversion Functions

We can solve this issue by creating runtime checks as functions that refine types appropriately:

```tsx
function isPercentage(input: number): input is Percentage {
  return input >= 0 && input <= 1
}
```

The `is` keyword indicates to Typescript that if this function returns `true`, then the input is a `Percentage` type ([known as a type predicate](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates)). You can then use this runtime check function to create a set of conversion functions:

```tsx
function toPercentage(input: number): Percentage | null {
  return isPercentage(input) ? input : null
}

function fromPercentage(input: Percentage): number {
  return input
}
```

You can now use these functions throughout your codebase if you need runtime checks for data. Then you'll get the benefits of compile-time checks for functions that you defined that require these specific values.

# Does this actually help?

For smaller applications, this level of type granularity is likely not particularly useful. However, as an application grows in complexity, compile-time type checking can seriously help prevent bugs related from misunderstanding the intention behind code (imagine looking at your own code from 6 months ago...).

Let's take our `setAlpha` example from above:

```tsx
function setAlpha(percentage: number) { ... }
```

It may seem obvious from the naming that this should be a value from 0 to 1, but consider potentially a value between 0% and 100%. I can understand someone thinking that `setAlpha(20)` is a reasonable use of the function. Only at runtime will we realize that something is wrong.

To further this problem, as your application gets even more complicated, you may not immediately notice the error during runtime. Only at a later time when you've forgotten about writing this could the issue pop up again unexpectedly.

By changing the type of the percentage parameter, we can enforce better compile-time type-checking in order to catch these issues earlier on. Now if we defined `setAlpha` with the more refined type:

```tsx
function setAlpha(percentage: Percentage) { ... }
```

Then we can get appropriate errors at compile-time when you try to pass in a generic number into the function. It will require either an explicit casting, or an assertion using our `isPercentage` function in order to make sure the type is correct.

This has the great benefit of allowing you to use the runtime check once for user input, and then have a compile-time guarantee for the data as it's passed through the system. Like so:

```tsx
// User input is a string from a text field
const percentage = parseFloat(userInput) // type: number
if (!isPercentage(percentage)) {
  // percentage is not of type `Percentage` and is therefore
  // and invalid input. Handle appropriate error code here.
  return
}

// percentage is type `Percentage` and has compile-time checking
// to confirm that it works with the `setAlpha` function
setAlpha(percentage)
```

If you were to take the same example, and remove the check for percentage's type, then you'd get a compile-time error from Typescript regarding the use of `number` for a parameter that requires a `Percentage` ([Typescript playground example](https://www.typescriptlang.org/play?ssl=15&ssc=42&pln=15&pc=58#code/PTAEBEFMDMEsDsEHNQBcAWlQAVICcBjSeVAQyS1QE8AHLU+AE1FNDwFcTYBbLAzAgGtQ0APZ5QsVAFgAUNTo58REuSwBeUPHbcARvlAAyUAG9QAfTJIAXKABEdQsSuQ7oAL5zonAqlij4SQBnXCdVCgAKBBp2VFttPXwASlto2OClMJdTOVA2SFR2PEC01FAAPk0ABiNjUtAAHk0ARjlPWTkQCBgEZFAggoBBABsadFZveF9-QNIglkDIAA9SbhphrFFoFlB2AdAtzJUXLx8-AP6h0fGIx2O1W1D7iiTTds6wAFUBiXqAd1E7GGzAo8HwpGGwyooAIol4IjwcJ2qGWqDgkGBBwkQThBXQyDksPgQTKe3wAEl4DEypo7AAWACsADoqs07HIPqAALQ80AAURWaw2oGatgAcqIYQJhGIJE9nGpuTyObIiSTQHcFRRmqBNDRSHgBgAxYaiUioCJkvCU6mvLoKSDxHT6PByAaoEZjUi3ZRayDNO1gPl4RESLlaZ0GWDzeCiMpzIKwJDwUi6YWoSXy8KQFVdHnhgWrdZYABMtgAwtK+rKjn6lVyVWqyprsyXdRqDcbTebLT8bbFA2haI6I4lXbJYNsIgBCaNZlw+rJqEtJV4mOQASC6Ley0a0cYO2wdoAABvO1CfNxhEX8tJBb8HQxE7BL46AAG4Q9hYfSoP6QYhQBqBhmDZJI2lzMAd0VPdjzPX1s0vWR3U9G5oIoFcgA)).

# Opaque Type Variations

Now that we've hopefully determined that this is generally a good idea, let's take it up a notch with some additional variations of opaque types. [All credit to _ProdigySim_ on Github for figuring this out](https://github.com/microsoft/TypeScript/issues/4895#issuecomment-425132582).

For the simplification of defining opaqueness, let's define a simple helper type:

```tsx
type Tag<T> = { _tag: T }
```

## Weak Opaque Type

This is the opaque type we've been talking about here with `Percentage`. It's uses the same definition as above (though cleaned up using the helper type this time):

```tsx
type Percentage = number & Tag<'percentage'>
```

This weak opaque type is useful because it can be downcasted into the base type of `number` in cases like passing it into a function while protecting from the incorrect use of `number` in cases where we need a `Percentage`. For example:

```tsx
function add(number): number
function setAlpha(Percentage): void

const num = 0.5 as number
const per = 0.5 as Percentage

add(num) // Works: number = number
add(per) // Works: Percentage downcasts to number

setAlpha(num) // Error: Cannot use a number when it expects a Percentage
setAlpha(per) // Works: Percentage = Percentage
```

## Strong Opaque Type

A strong opaque type is defined a little bit differently:

```tsx
type Percentage = (number & Tag<'percentage'>) | Tag<'percentage'>
```

_Key difference is the addition of `| Tag<"percentage">` at the end._

Strong opaque types have the additional restriction of requiring explicit casting in order to be used as their base types (`number` in this example). This means that we wouldn't be able to pass a `Percentage` into the `add` function above without explicitly casting it first:

```tsx
function add(number): number
const per = 0.5 as Percentage

add(per) // Error: Percentage is incompatible with number
add(per as number) // Works: Percentage explicitly casted to number
```

This can be useful in cases where you don't accidentally want to use the more specific type for more general cases without a clear exception being made via explicit casting.

## Super Opaque Type

A super opaque type has the simplest definition of them all:

```tsx
type Percentage = Tag<'percentage'>
```

Super opaque types have the most stringent typings and cannot be implicitly or explicitly casted to their base types. This means that we need to cast the type to `any` first before we can cast it to the base type for use:

```tsx
function add(number): number
const per = 0.5 as Percentage

add(per) // Error: Percentage is incompatible with number
add(per as number) // Error: Percentage does not sufficiently overlap number
add(per as any as number) // Works: Ignoring the inherent types to allow Percentage to be used in place of number
```

This is useful for cases where you are truly making a new type that is unrelated any base type. This is like comparing `number` to `string`.

_Note: Super Opaque Type boils down to just typing the data as an object with a special key, thus this can cause unexpected edge-cases when functions accept any object._

## Defining Opaque Helper Types

From these different opaque types, we can define some simple helper types that reflect the different ways we can define an opaque type within Typescript:

```tsx
type Tag<T> = { _tag: T }
type WeakOpaqueType<BaseType, T> = BaseType & Tag<T>
type StrongOpaqueType<BaseType, T> = (BaseType & Tag<T>) | Tag<T>
type SuperOpaqueType<T> = Tag<T>
```

You can copy and use these helper types within your own code in order to begin defining your own custom types. Good luck!

# 3rd Party Libraries

Now you may be wondering if there are any prebuilt solutions for you to use within your code. I'm happy to say that there is! One notable library I've come across is [taghiro](https://github.com/sveseme/taghiro). It defines several numeric and string types (like UUID, ascii, and regex) with corresponding validation functions to allow for assertions within your code that refines the types appropriately.

Something to note from the library is that all of the exported types are super opaque types while all of the assertions refine into weak opaque types.

_Note: I’m not connected to development of `taghiro`. Use at your own risk._