unique types in TypeScript using "branding"

skybrian
2024-01-03T01:07:21Z

I'm doing a bit of Typescript programming and started using zod for input validation. It has a fair number of convenience methods, one of which is brand(). This creates a unique type, much like the newtype operator in some languages. This is despite TypeScript not having unique types by default; TypeScript implements structural typing.

The technique used to implement unique types has been known for a long time, but it's new to me. You can declare a type with an extra field that doesn't really exist.

There seem to be several variations on how to do this. They seem to be mostly equivalent, but the ergonomics might differ. (Some might have better compiler errors that others?) The basic requirement is that the imaginary field doesn't get in the way in normal use, but it's incompatible with other types, causing a compiler error if they're mixed.

One way goes like this:

declare const brand: unique symbol;

type Brand<T, TBrand extends string> = T & {
  [brand]: TBrand;
}

It can be used to declare branded types like this:

type TopicId = Brand<bigint, "TopicId">;

const myTopicId = 123n as TopicId;

This trick relies on the fact that TypeScript's type checking is unsound. We can lie to the type checker. Intersecting T (in this case, bigint) creates a subtype of bigint with an imaginary field. The field's type is declared so the field requires a specific string, so it's going to be incompatible with just about any other type, unless you use Brand to give it the same name on purpose. (The field doesn't actually exist and no string is actually stored there.)

To create a TopicId value, you use "as" to explicitly downcast bigints to TopicId's. Then you can use them just like a bigint, except that there will be compile errors if you use them wrong.

A less strict approach is to make the imaginary field optional, described here as "flavoring" but I don't think that's in common use? Then you could assign bignums without doing a cast, but the type is still incompatible with other branded type. This reminds me of how types work in Go.

Comments URL: https://tildes.net/~comp/1da4/unique_types_in_typescript_using_branding

Votes: 11

Comments: 1