The Next Generation of Branded Types in TypeScript
andrew jarrett

andrew jarrett @ahrjarrett

Location:
Austin, TX
Joined:
Apr 28, 2021

The Next Generation of Branded Types in TypeScript

Publish Date: Jun 20
0 0

This post will be shorter than most, mostly because writing this has been on my todo list since I released the newtype type 11 months ago, and since it came up in the TypeScript discord yesterday.

If you're unfamiliar with branded types in TypeScript, this article is not meant to be an introduction to them -- I recommend you read Josh Goldberg's chapter on Branded Types, and then come back.

tl;dr, about a year ago I released a type called newtype, that lets you define types like this:

import type { newtype } from "any-ts"

declare namespace integer {
  const URI: unique symbol
}

interface integer extends newtype<
  number & { [integer.URI]: never }
> {}

declare const myInt: integer

const ex_01 = myInt + 2.3
// 🚫 TypeError ^^:
//    Operator '+' cannot be applied to types
//    'integer' and 'number'

// Downcast `myInt` with the unary plus operator:
const ex_02 = +myInt + 2.3
//    ^? const ex_02: number

Enter fullscreen mode Exit fullscreen mode

Play with it in the TypeScript Playground

What's happening here?

The newtype type is part of a library I maintain called any-ts.

any-ts is a TypeScript library, but different than the ones you usually see.

Rather than shipping a bunch of fancy TypeScript utility types, it ships a bunch of TypeScript primitives.

One of those primitives is newtype. If you're familiar with Rust or Haskell, this version of newtype is like a poor person's newtype.

Despite that, it's much more powerful than it looks at first glance. But since this article isn't about promoting my library -- we'll skip them for now.

Suffice to say, one of the things newtype lets you do is wrap up a primitive type (like a string or number) in a TypeScript interface.

By doing that, the name you give the type "sticks".

In other words, instead of this:

type Integer = number & { [IntegerSymbol]: never }

declare let myInt: Integer
//          ^? let myInt: number { [IntegerSymbol]: never }      
Enter fullscreen mode Exit fullscreen mode

...which is harder to read and leaks implementation details, you get this:

interface integer extends newtype<number> {}

declare let myInt: integer
//          ^? let myInt: integer
Enter fullscreen mode Exit fullscreen mode

Notably, you can implement every "flavor" (pun intended) of branded type that Josh outlines in his chapter on Branded Types, in terms of the newtype pattern.

If you're curious and would like to learn more, I recommend:

And of course if you have any questions for me, I'm totally open to chatting. I'm on GitHub, LinkedIn, and you can always reach my by email at ahrjarrett at gmail dot com.

Comments 0 total

    Add comment