Β· hands on
Understanding Branded Types in TypeScript
Branded types in TypeScript help create unique types to prevent errors. By using a unique marker, type guards and assertion functions help enforce these branded types for added safety.
Branded types, also known as "nominal typing," allow you to create distinct types that cannot be mixed with other types, even if their underlying structures are the same. This is particularly useful when you want to prevent logical errors that type checks do not catch.
Contents
- Problem Statement
- Creating a Branded Type
- Define the Brand utility type
- Using Type Guards with Branded Types
- Using Assertion Functions with Branded Types
- Video Tutorial
Problem Statement
In the code example below, the divide
function can accept any number for both parameters. This poses a risk of division by zero if the value of b
is zero, which can lead to unintended consequences like returning "Infinity":
By implementing branded types, we can avoid the accidental use of 0
for b
at the type level, enhancing type safety and avoiding such errors.
Creating a Branded Type
A branded type in TypeScript can be implemented using an intersection type that combines a base type with a unique marker. Hereβs how you can define a branded type using a type alias named NetZero
and an intersection type (designated by the &
symbol):
The resulting type is an intersection of number
and an object with a unique __brand
property. This property acts as a unique tag, ensuring that the branded type is distinct from its base type and other branded types. The name __brand
is conventional and sometimes can also have different names such as __type
.
Define the Brand utility type
If you want to use branded types more often, you can create a utility type that helps you creating more branded types with ease:
Using Type Guards with Branded Types
The simplest way to convert a primitive type into a branded type is by using a custom type guard. A type guard in TypeScript leverages the is
keyword to predict a type, helping the TypeScript compiler recognize that our variables meet the requirements of our branded type:
The benefit of using the isNotZero
type guard is that it ensures the variable is checked prior to being passed to the divide
function. Since divide
mandates that b
be of the type NotZero
, we cannot directly pass any number; it must first pass through the isNotZero
check. While this does require an if-statement, which may slightly complicate the readability by introducing additional code blocks, it offers the safety of not crashing the program if the check fails.
An alternative to employing a type guard is to use an assertion function. This approach eliminates the need for an if-statement, streamlining the code flow. However, if the assertion fails, it could crash the program, thus it is crucial to encapsulate it within a try-catch
block to manage potential runtime exceptions. This trade-off between readability and robust error handling is a key consideration in choosing between a type guard and an assertion function.
Using Assertion Functions with Branded Types
Assertion functions in TypeScript are used to perform runtime checks and signal to the compiler what type a particular value is, if the checks succeed:
In this setup, the asserts
keyword transforms our type predicate into an assertion, indicating to TypeScript that if assertNotZero
completes without throwing an error, the input can be reliably treated as type NotZero
. Once our variables pass through this function, the TypeScript compiler will permit their use in subsequent code blocks with the inferred type of NotZero
:
Branded types ensure that we enforce a safety check on the type of b
before it is used in the divide
function. This mitigates the risk of errors due to improper type usage.