TypeScript's structural type system allows any two types that seem to have the same shape to be assignable to each other. But what if you want to restrict a type to only allow certain values, even if other values happen to have the same structure? Say, marking a difference between sanitized and un-sanitized strings, or positive integers from all numbers?
This need is solvable with a pattern called "branded types", sometimes also called "opaque types". Let's dive into why you'd want to use branded types, how to declare and use them, and some alternatives to the pattern.
Branding Needsโ
TypeScript's type system doesn't always provide a way to differentiate types that seem to be structurally the same. For example, some values in application might need to work only with positive numbers. Assigning any number that isn't known to be zero or positive should be a type error:
ts
ts
TypeScript doesn't have a built-in way to describe positive numbers, but we still want to indicate them in the type system. In other words, we need a way to "brand" (mark) some number as being not just any old number, but specifically the type we want.
Introducing Branded Typesโ
A "branded type" is one that is the same as an existing type, but with some extra type system property that doesn't actually exist at runtime. That "brand" property is used to differentiate the two types in the type system.
As an example, we can declare a branded type for positive numbers by declaring a Positive type that is both a number and an object with a never-used __brand property.
The values we'll later use as Positives won't actually have a __brand.
The __brand property is just in the type system.
We can then use that Positive branded type in place of number for any location that should only ever receive a positive number:
tsPositive = number & {__brand : "positive" };ยdeclare functionwaitForSeconds (seconds :Positive ):Promise <void>;ยasync functionwaitThenLog (seconds :Positive ) {// Now, this errors as we'd expect it to. ๐Argument of type 'number' is not assignable to parameter of type 'Positive'. Type 'number' is not assignable to type '{ __brand: "positive"; }'.2345Argument of type 'number' is not assignable to parameter of type 'Positive'. Type 'number' is not assignable to type '{ __brand: "positive"; }'.console .log (waitForSeconds (-1));ย// This should not be an error: a guid is being used. ๐console .log (waitForSeconds (seconds ));ยconsole .log ("Done!");}
tsPositive = number & {__brand : "positive" };ยdeclare functionwaitForSeconds (seconds :Positive ):Promise <void>;ยasync functionwaitThenLog (seconds :Positive ) {// Now, this errors as we'd expect it to. ๐Argument of type 'number' is not assignable to parameter of type 'Positive'. Type 'number' is not assignable to type '{ __brand: "positive"; }'.2345Argument of type 'number' is not assignable to parameter of type 'Positive'. Type 'number' is not assignable to type '{ __brand: "positive"; }'.console .log (waitForSeconds (-1));ย// This should not be an error: a guid is being used. ๐console .log (waitForSeconds (seconds ));ยconsole .log ("Done!");}
By marking our guid numbers as the Positive type, we indicated to TypeScript that non-positive numbers shouldn't be used in their place.
In doing so we achieved more safety in our code - at the cost of writing a few more types.
Branded types are a useful lie to the type system: our positive numbers will never actually have that __brand property.
We're just making sure that no developer accidentally provides a value of a non-branded type to a location that requires one that is branded.
Branding Valuesโ
The previous code showed passing around values that are already known to be a branded type. But how do we tell the type system that some new value is known to a branded type?
Creating a new value that is used as a branded type generally requires asserting to TypeScript that the value does, in fact, satisfy the branded type's guarantees.
This is often done in one of two ways: with an as assertion or with a utility function.
as Assertionsโ
Type assertions are a way of telling TypeScript something it wouldn't be able to know from code on its own.
In the case of branded values, as can be used to tell TypeScript that a value is intended to be an instance of a branded type.
The following snippet uses an as assertion to tell TypeScript that the 123 is, in fact, a Positive:
tsPositive = number & {__brand : "positive" };ยletmyPositive :Positive ;ย// Ok: the assertion tells TypeScript we meant this. ๐myPositive = 123 asPositive ;ย// Type error: TypeScript doesn't know whether this is intended. ๐Type 'number' is not assignable to type 'Positive'. Type 'number' is not assignable to type '{ __brand: "positive"; }'.2322Type 'number' is not assignable to type 'Positive'. Type 'number' is not assignable to type '{ __brand: "positive"; }'.= 123; myPositive 
tsPositive = number & {__brand : "positive" };ยletmyPositive :Positive ;ย// Ok: the assertion tells TypeScript we meant this. ๐myPositive = 123 asPositive ;ย// Type error: TypeScript doesn't know whether this is intended. ๐Type 'number' is not assignable to type 'Positive'. Type 'number' is not assignable to type '{ __brand: "positive"; }'.2322Type 'number' is not assignable to type 'Positive'. Type 'number' is not assignable to type '{ __brand: "positive"; }'.= 123; myPositive 
By using an as assertion, the code told TypeScript that the 123 value should be considered a Positive.
Notice that no __brand property was ever assigned at runtime.
The as assertion told TypeScript to assume it exists.
Using as assertions for branded types is a convenient way to quickly assert a value to be a branded type.
Unfortunately, as assertions can bypass type errors in cases where the developer might not be correct.
The previous code could have written something blatantly incorrect, like -1 as Positive, and TypeScript would have been none the wiser.
Be very careful whenever using as assertions in TypeScript.
Type Predicatesโ
Instead of writing as assertions to create new instances of branded types, it's often safer -though more laborious- to use functions that validate values before they can be considered instances of branded types.
"Type predicates" are functions whose return type indicates whether a parameter is a particular type.
Although they return a boolean value at runtime, the type system knows to apply type narrowing based on the returned value.
Type predicates can be used to return whether a parameter is a particular branded type.
This isPositive returns a boolean indicating whether value is Positive.
That means inside of the following if (isPositive(value)) {, TypeScript knows the value is of type Positive:
tsPositive = number & {__brand : "positive" };ยfunctionisPositive (value : number):value isPositive {returnvalue > 0;}ยletmyPositive :Positive ;ยletvalue = 123;ยif (isPositive (value )) {// Ok: the type predicate tells TypeScript we meant this. ๐myPositive =value ;}ย// Type error: TypeScript doesn't know whether this is intended. ๐Type 'number' is not assignable to type 'Positive'. Type 'number' is not assignable to type '{ __brand: "positive"; }'.2322Type 'number' is not assignable to type 'Positive'. Type 'number' is not assignable to type '{ __brand: "positive"; }'.= 123; myPositive 
tsPositive = number & {__brand : "positive" };ยfunctionisPositive (value : number):value isPositive {returnvalue > 0;}ยletmyPositive :Positive ;ยletvalue = 123;ยif (isPositive (value )) {// Ok: the type predicate tells TypeScript we meant this. ๐myPositive =value ;}ย// Type error: TypeScript doesn't know whether this is intended. ๐Type 'number' is not assignable to type 'Positive'. Type 'number' is not assignable to type '{ __brand: "positive"; }'.2322Type 'number' is not assignable to type 'Positive'. Type 'number' is not assignable to type '{ __brand: "positive"; }'.= 123; myPositive 
Type predicates can be useful for safely gating some logic on whether a value matches a branded type's intent.
But, keep in mind that type predicates aren't much more type-safe than an as assertion.
TypeScript doesn't check whether the logic of a type predicate matches up to a branded type's intent.
It leaves that up to the code author.
Type Assertion Functionsโ
Another downside of type predicate functions is that they require writing conditional wrappers in code (the if (isPositive) { ... }).
Those ifs take up more space and add more runtime logic than as assertions.
To make our code a little more readable, we can extract a utility function known as a "type assertion function". A type assertion function takes in a value, throws an error if the value doesn't match an expected type, and otherwise returns the value. When paired with a type predicate function, a type assertion function can inform TypeScript's type system that a value is an instance of a branded type.
This asPositive function uses isPositive to throw an error if the value isn't an integer.
The result is that calling code can wrap a value with asPositive to assert that that value is definitely a Positive:
tsPositive = number & {__brand : "positive" };ยfunctionisPositive (value : number):value isPositive {returnvalue > 0;}ยfunctionasPositive (value : number):Positive {if (!isPositive (value )) {throw newError (`${value } is not positive.`);}ยreturnvalue ;}ยletmyPositive :Positive ;ย// Ok: the type assertion function enforces that it's a `Positive`. ๐myPositive =asPositive (123);ย// Type error: TypeScript doesn't know whether this is intended. ๐Type 'number' is not assignable to type 'Positive'. Type 'number' is not assignable to type '{ __brand: "positive"; }'.2322Type 'number' is not assignable to type 'Positive'. Type 'number' is not assignable to type '{ __brand: "positive"; }'.= 123; myPositive 
tsPositive = number & {__brand : "positive" };ยfunctionisPositive (value : number):value isPositive {returnvalue > 0;}ยfunctionasPositive (value : number):Positive {if (!isPositive (value )) {throw newError (`${value } is not positive.`);}ยreturnvalue ;}ยletmyPositive :Positive ;ย// Ok: the type assertion function enforces that it's a `Positive`. ๐myPositive =asPositive (123);ย// Type error: TypeScript doesn't know whether this is intended. ๐Type 'number' is not assignable to type 'Positive'. Type 'number' is not assignable to type '{ __brand: "positive"; }'.2322Type 'number' is not assignable to type 'Positive'. Type 'number' is not assignable to type '{ __brand: "positive"; }'.= 123; myPositive 
Type assertion functions can be a handy pattern for writing more succinct code with branded types. However, they introduce a new downside of relying on throwing errors to indicate when values aren't the correct type. Thrown errors can't be made type-safe.
As with as assertions, be very careful whenever using type assertion functions in TypeScript.
Uses for Branded Typesโ
Branded types can be helpful whenever TypeScript should be told that two seemingly-identical structures are actually different. The following three sections are are non-exhaustive list of use cases for branded types.
Branded Numbersโ
Numbers are some of the most common uses for branded types because TypeScript doesn't support numeric types more specific than bigint or integer.
Existing proposals such as Microsoft/TypeScript#54925 Numeric Range Types (Feature Update) are still in discussion.
Other numeric variants such as Negative, NonZero, and Prime on can all be represented with branded types.
Currency handling is another useful case for branded numeric types. Applications dealing with multiple currencies might need to make sure numbers used for amounts of one currency don't accidentally get transferred to a different currency.
This code uses a generic Currency<T> type to set up multiple branded types for currencies:
tsCurrency <T > = number & {__currency :T };typeEuro =Currency <"euro">;typeUSD =Currency <"usd">;ยinterfacePurchasableEuro {cost :Euro ;name : string;}ยconstmyPrice = 10 asUSD ;ยconstgyro :PurchasableEuro = {Type 'USD' is not assignable to type 'Euro'. Type 'USD' is not assignable to type '{ __currency: "euro"; }'. Types of property '__currency' are incompatible. Type '"usd"' is not assignable to type '"euro"'.2322Type 'USD' is not assignable to type 'Euro'. Type 'USD' is not assignable to type '{ __currency: "euro"; }'. Types of property '__currency' are incompatible. Type '"usd"' is not assignable to type '"euro"'.: cost myPrice ,name : "euro",};
tsCurrency <T > = number & {__currency :T };typeEuro =Currency <"euro">;typeUSD =Currency <"usd">;ยinterfacePurchasableEuro {cost :Euro ;name : string;}ยconstmyPrice = 10 asUSD ;ยconstgyro :PurchasableEuro = {Type 'USD' is not assignable to type 'Euro'. Type 'USD' is not assignable to type '{ __currency: "euro"; }'. Types of property '__currency' are incompatible. Type '"usd"' is not assignable to type '"euro"'.2322Type 'USD' is not assignable to type 'Euro'. Type 'USD' is not assignable to type '{ __currency: "euro"; }'. Types of property '__currency' are incompatible. Type '"usd"' is not assignable to type '"euro"'.: cost myPrice ,name : "euro",};
Per Microsoft/TypeScript#59423 Don't allow math operations on different branded numeric types, binary operations on branded numeric types don't cause any type errors. Use with caution.
Branded Stringsโ
Other uses for branded string types include decoded or sanitized text, such as user input that has been sanitized against injected code, passwords, and HTML or URL encoded characters. Branded types can be used to enforce that un-sanitized strings aren't provided in locations that should only use sanitized strings.
This snippet shows protecting against XSS attacks by forcing user input to be sanitized before appending to the DOM:
tsSafeString = string & {__sanitized : true };ย/*** Removes any unsafe parts, such as <script> tags.*/declare functionsanitize (xml : string):SafeString ;ยfunctionwriteToDocument (xml :SafeString ) {document .body .innerHTML +=xml ;}ยconstuserInput = `<script src="evil.js"></script>`;ย// Ok: the text is sanitized and safe for the page ๐writeToDocument (sanitize (userInput ));ย// Type error: unsafe input XML ๐Argument of type 'string' is not assignable to parameter of type 'SafeString'. Type 'string' is not assignable to type '{ __sanitized: true; }'.2345Argument of type 'string' is not assignable to parameter of type 'SafeString'. Type 'string' is not assignable to type '{ __sanitized: true; }'.writeToDocument (); userInput 
tsSafeString = string & {__sanitized : true };ย/*** Removes any unsafe parts, such as <script> tags.*/declare functionsanitize (xml : string):SafeString ;ยfunctionwriteToDocument (xml :SafeString ) {document .body .innerHTML +=xml ;}ยconstuserInput = `<script src="evil.js"></script>`;ย// Ok: the text is sanitized and safe for the page ๐writeToDocument (sanitize (userInput ));ย// Type error: unsafe input XML ๐Argument of type 'string' is not assignable to parameter of type 'SafeString'. Type 'string' is not assignable to type '{ __sanitized: true; }'.2345Argument of type 'string' is not assignable to parameter of type 'SafeString'. Type 'string' is not assignable to type '{ __sanitized: true; }'.writeToDocument (); userInput 
Writing to innerHTML is a dangerous practice in general.
Treat this as an educational example, not an endorsement of insecure HTML practices.
Strings can also be used as "guid" (Globally Unique ID) types or similar identifying properties in code. Some applications declare branded GUID types for every shape of data in their database, to ensure they can't be interchanged with each other.
This snippet differentiates comment IDs and post IDs with branded Guid types:
tsGuid <DataType > = string & {__guid :DataType };ยtypeCommentId =Guid <"comment">;typePostId =Guid <"post">;ยinterfaceComment {id :CommentId ;// ...}ยinterfacePost {id :PostId ;// ...}ยdeclare functiongetCommentById (id :CommentId ):Promise <Comment >;declare functiongetPostById (id :PostId ):Promise <Post >;ยasync functiongetById (id :CommentId ) {// Ok: the branded type matches up ๐awaitgetCommentById (id );ย// Type error: mismatched comment and post ๐awaitArgument of type 'CommentId' is not assignable to parameter of type 'PostId'. Type 'CommentId' is not assignable to type '{ __guid: "post"; }'. Types of property '__guid' are incompatible. Type '"comment"' is not assignable to type '"post"'.2345Argument of type 'CommentId' is not assignable to parameter of type 'PostId'. Type 'CommentId' is not assignable to type '{ __guid: "post"; }'. Types of property '__guid' are incompatible. Type '"comment"' is not assignable to type '"post"'.getPostById (); id }
tsGuid <DataType > = string & {__guid :DataType };ยtypeCommentId =Guid <"comment">;typePostId =Guid <"post">;ยinterfaceComment {id :CommentId ;// ...}ยinterfacePost {id :PostId ;// ...}ยdeclare functiongetCommentById (id :CommentId ):Promise <Comment >;declare functiongetPostById (id :PostId ):Promise <Post >;ยasync functiongetById (id :CommentId ) {// Ok: the branded type matches up ๐awaitgetCommentById (id );ย// Type error: mismatched comment and post ๐awaitArgument of type 'CommentId' is not assignable to parameter of type 'PostId'. Type 'CommentId' is not assignable to type '{ __guid: "post"; }'. Types of property '__guid' are incompatible. Type '"comment"' is not assignable to type '"post"'.2345Argument of type 'CommentId' is not assignable to parameter of type 'PostId'. Type 'CommentId' is not assignable to type '{ __guid: "post"; }'. Types of property '__guid' are incompatible. Type '"comment"' is not assignable to type '"post"'.getPostById (); id }
Branded string types can be useful whenever data from different sources is stored as similar-looking strings, but shouldn't be mixed together.
Community Librariesโ
Branded types aren't used in most TypeScript projects, but they are handy enough that a few community projects have sprung up to make using them easier.
ts-brand and Effect TS are two of the more popular ones.
ts-brandโ
The ts-brand package provides a community-built option to share pre-written code just for type brands.
It exports a generic Brand type that can be used to create branded types:
ts
ts
ts-brand comes with several utilities around branded types.
Its make function notably creates a utility function that asserts a value to be a branded type.
This asPositive is an "identity" function (one that directly returns its argument) with a return type of Positive instead of number:
tsBrand ,make } from "ts-brand";ยtypePositive =Brand <number, "positive">;ยconstasPositive =make <Positive >();ยconstvalue =asPositive (42);
tsBrand ,make } from "ts-brand";ยtypePositive =Brand <number, "positive">;ยconstasPositive =make <Positive >();ยconstvalue =asPositive (42);
make can take in an optional type assertion function to enforce provided values match some constraint.
You can use those to enforce the constraints as part of make's identity functions.
This asPositive throws an error if the provided number isn't greater than zero:
ts
ts
See the ts-brand API docs for more details.
Effect TSโ
Effect is a popular framework for building type-rich TypeScript applications.
One of the many utilities it provides is a Brand including:
- Brand.Brand: a generic type that acts as the brand in a type
- Brand.nominal: a generic function that returns a type brand identify function
Put together, the two allow making type brands similar to ts-brand:
tsBrand } from "effect";ยtypePositive = number &Brand .Brand <"Positive">;ยconstasPositive =Brand .nominal <Positive >();ยletmyPositive :Positive ;ย// Ok: the asPositive tells TypeScript we meant this. ๐myPositive =asPositive (123);ย// Type error: TypeScript doesn't know whether this is intended. ๐Type 'number' is not assignable to type 'Positive'. Type 'number' is not assignable to type 'Brand<"Positive">'.2322Type 'number' is not assignable to type 'Positive'. Type 'number' is not assignable to type 'Brand<"Positive">'.= 123; myPositive 
tsBrand } from "effect";ยtypePositive = number &Brand .Brand <"Positive">;ยconstasPositive =Brand .nominal <Positive >();ยletmyPositive :Positive ;ย// Ok: the asPositive tells TypeScript we meant this. ๐myPositive =asPositive (123);ย// Type error: TypeScript doesn't know whether this is intended. ๐Type 'number' is not assignable to type 'Positive'. Type 'number' is not assignable to type 'Brand<"Positive">'.2322Type 'number' is not assignable to type 'Positive'. Type 'number' is not assignable to type 'Brand<"Positive">'.= 123; myPositive 
Effect also allows making a type brand assertion function.
Unlike ts-brand's make, Effect provides a separate function, Brand.refined, that takes in two parameters:
- A function that determines whether a value matches the type brand's constraint
- A function to throw an error for a value that doesn't match the constraint
This asPositive throws an error if the provided number isn't greater than zero:
tsBrand } from "effect";ยtypePositive = number &Brand .Brand <"Positive">;ยconstasPositive =Brand .refined <Positive >((value ) =>value > 0,(value ) =>Brand .error (`Non-positive value: ${value }`));ย// Ok ๐asPositive (42);ย// Error: Non-positive value: -1 ๐asPositive (-1);
tsBrand } from "effect";ยtypePositive = number &Brand .Brand <"Positive">;ยconstasPositive =Brand .refined <Positive >((value ) =>value > 0,(value ) =>Brand .error (`Non-positive value: ${value }`));ย// Ok ๐asPositive (42);ย// Error: Non-positive value: -1 ๐asPositive (-1);
See Effect's Branded Types docs for more details.
Alternatives to Type Brandsโ
Type brands, as with all type system features, add complexity to your code. The benefits of type brands might not outweigh the drawback of that added complexity!
Before using type brands, think on whether they solve any actual issues you're likely to face in your application. You might be better off with a different strategy, such as one of the following ones.
Avoiding Type Brands Altogetherโ
Your type system doesn't have to document every facet of your code. In fact, no type system can fully describe all the nuances of your values. Attempting to do so can sometimes lead to overly complex code or type definitions. Sometimes it's easier to stick with basic type primitives and live with slightly imprecise types.
For example, in an application that operates with numeric values that must always be non-zero, it can be tempting to make a NonZero branded type to enforce that.
But if values are known by developers to never be zero, it might be easier to just sprinkle in an occasional assertion.
Instead of the following NonZero type brand and assertion functions with a divide function...
ts
ts
...using a single thrown error inside divide results in equivalently safe and much more readable code:
tsdivide (a : number,b : number) {if (b !== 0) {throw newError ("Cannot divide by zero.");}ยreturna /b ;}ยconsole .log (divide (12, 34));
tsdivide (a : number,b : number) {if (b !== 0) {throw newError ("Cannot divide by zero.");}ยreturna /b ;}ยconsole .log (divide (12, 34));
When deciding whether to add branded types to your code, use your best judgement on the tradeoffs. Branded types can help precisely describe code -- at the cost of extra verbosity.
Unionsโ
If a value is known to be one of a limited set of possible types, the best description for it might be a | union type.
Union types are made to describe a specific set of known allowed types.
As an example, take a Task class that stores its current status as a status string.
One version of the class could represent that status as a branded TaskStatus type to differentiate it from other strings:
tsTaskStatus = string & {__brand : "status" };ยclassTask {#status:TaskStatus = "pending" asTaskStatus ;ยconstructor(worker : () =>Promise <void>) {worker ().then (() => (this.#status = "resolved" asTaskStatus )).catch (() => (this.#status = "rejecting" asTaskStatus ));// ~~~~~~~~~~~// This should be "rejected", but there is no type error. ๐}ยgetStatus () {return this.#status;}}
tsTaskStatus = string & {__brand : "status" };ยclassTask {#status:TaskStatus = "pending" asTaskStatus ;ยconstructor(worker : () =>Promise <void>) {worker ().then (() => (this.#status = "resolved" asTaskStatus )).catch (() => (this.#status = "rejecting" asTaskStatus ));// ~~~~~~~~~~~// This should be "rejected", but there is no type error. ๐}ยgetStatus () {return this.#status;}}
Cases where a value must be one of a set of known types are generally better represented by a union type. Union types are more precise for describing exactly what a value might be.
The TaskStatus type would be better off as a union of the three known task status strings.
Doing so would have caught the incorrect string in the previous example:
tsTaskStatus = "pending" | "rejected" | "resolved";ยclassTask {#status:TaskStatus = "pending";ยconstructor(worker : () =>Promise <void>) {worker ().then (() => (this.#status = "resolved")).catch (() => (this.#status = "rejected"));}ยgetStatus () {return this.#status;}}
tsTaskStatus = "pending" | "rejected" | "resolved";ยclassTask {#status:TaskStatus = "pending";ยconstructor(worker : () =>Promise <void>) {worker ().then (() => (this.#status = "resolved")).catch (() => (this.#status = "rejected"));}ยgetStatus () {return this.#status;}}
Whenever a value can only be one of a set of known values, consider using a union type to represent that set.
Enumsโ
Another TypeScript construct for representing a known set of values is an enum. Enums, enumerated type, are similar to unions in that they allow describing a set of related types under one shared name.
Unlike unions, enums are one of the few runtime syntax extensions provided by TypeScript. Each enum declared in code becomes an object. Values under the enum are referred to under the enum's name -- both when used as a type and when as a runtime value.
The TaskStatus example could be written with an enum TaskStatus:
tsTaskStatus {Pending = "pending",Rejected = "rejected",Resolved = "resolved",}ยclassTask {#status =TaskStatus .Pending ;ยconstructor(worker : () =>Promise <void>) {worker ().then (() => (this.#status =TaskStatus .Resolved )).catch (() => (this.#status =TaskStatus .Rejected ));}ยgetStatus () {return this.#status;}}
tsTaskStatus {Pending = "pending",Rejected = "rejected",Resolved = "resolved",}ยclassTask {#status =TaskStatus .Pending ;ยconstructor(worker : () =>Promise <void>) {worker ().then (() => (this.#status =TaskStatus .Resolved )).catch (() => (this.#status =TaskStatus .Rejected ));}ยgetStatus () {return this.#status;}}
Enums are a bit contentious to some TypeScript developers. They add new runtime code that may eventually conflict with later versions of JavaScript, such as if the TC39 Enums Proposal is ever ratified. On the one hand, they can be useful for having a set of types and values under a shared name.
Template Literal Typesโ
TypeScript allows types describing general patterns of strings called template literal types. These types are handy when you know a string must match some pattern, but it'd be cumbersome to write out all permutations of that pattern in a union or enum.
The following ThemeStore class uses a branded string type named Theme to represent its themes.
However, Theme doesn't enforce any pattern for the contents, even though the code comment suggests it should be a specific shape of string:
tsTheme = string & {__brand : "theme" };ยclassThemeStore {#theme:Theme ;ยconstructor(theme :Theme ) {this.#theme =theme ;}ยgetTheme () {return this.#theme;}ยsetTheme (theme :Theme ) {this.#theme =theme ;}}ยconststore = newThemeStore ("dark-standard" asTheme );ยstore .setTheme ("dark-high" asTheme );ยstore .setTheme ("dark-regular" asTheme );// ~~~~~~~~~~~~~// This should be "dark-standard", but there is no type error. ๐
tsTheme = string & {__brand : "theme" };ยclassThemeStore {#theme:Theme ;ยconstructor(theme :Theme ) {this.#theme =theme ;}ยgetTheme () {return this.#theme;}ยsetTheme (theme :Theme ) {this.#theme =theme ;}}ยconststore = newThemeStore ("dark-standard" asTheme );ยstore .setTheme ("dark-high" asTheme );ยstore .setTheme ("dark-regular" asTheme );// ~~~~~~~~~~~~~// This should be "dark-standard", but there is no type error. ๐
Switching Theme to a template literal string allows TypeScript to give a type error when a string of the wrong pattern is provided:
tsThemeBase = "dark" | "light" | "system";typeThemeContrast = "high" | "low" | "standard";typeTheme = `${ThemeBase }-${ThemeContrast }`;ยclassThemeStore {#theme:Theme ;ยconstructor(theme :Theme ) {this.#theme =theme ;}ยgetTheme () {return this.#theme;}ยsetTheme (theme :Theme ) {this.#theme =theme ;}}ยconststore = newThemeStore ("dark-standard");ยstore .setTheme ("dark-high");ยArgument of type '"dark-regular"' is not assignable to parameter of type '"dark-high" | "dark-low" | "dark-standard" | "light-high" | "light-low" | "light-standard" | "system-high" | "system-low" | "system-standard"'.2345Argument of type '"dark-regular"' is not assignable to parameter of type '"dark-high" | "dark-low" | "dark-standard" | "light-high" | "light-low" | "light-standard" | "system-high" | "system-low" | "system-standard"'.store .setTheme ("dark-regular" );
tsThemeBase = "dark" | "light" | "system";typeThemeContrast = "high" | "low" | "standard";typeTheme = `${ThemeBase }-${ThemeContrast }`;ยclassThemeStore {#theme:Theme ;ยconstructor(theme :Theme ) {this.#theme =theme ;}ยgetTheme () {return this.#theme;}ยsetTheme (theme :Theme ) {this.#theme =theme ;}}ยconststore = newThemeStore ("dark-standard");ยstore .setTheme ("dark-high");ยArgument of type '"dark-regular"' is not assignable to parameter of type '"dark-high" | "dark-low" | "dark-standard" | "light-high" | "light-low" | "light-standard" | "system-high" | "system-low" | "system-standard"'.2345Argument of type '"dark-regular"' is not assignable to parameter of type '"dark-high" | "dark-low" | "dark-standard" | "light-high" | "light-low" | "light-standard" | "system-high" | "system-low" | "system-standard"'.store .setTheme ("dark-regular");
Whenever strings are guaranteed to match a particular format, template literal types can be a precise tool for describing their format in the type system.
Closing Thoughtsโ
Branded types are a nifty trick in the type system for differentiating structurally identical types from each other. They have quite a few use cases that can help increase program safety.
However, they come with the cost of increased conceptual complexity for working with your code. Whether you're writing your own branded types, onboarding a community library, or opting for an alternative strategy instead, you've got options for using types to precisely describe the characteristics of your values.
Further Readingโ
- Narrowing is covered in Learning TypeScript Chapter 3: Unions and Literals.
- Type predicates are covered in Learning TypeScript Chapter 9: Type Modifiers.
- Enums are covered in Learning TypeScript Chapter 14: Syntax Extensions.
- Template literal types are covered in Learning TypeScript Chapter 15: Type Operations.
Got your own TypeScript questions? Tweet @LearningTSBook and the answer might become an article too!