How to define an opaque type in TypeScript?

That is because TypeScript type system is “structural”, so any two types with the same shape will be assignable one to each other – as opposed to “nominal”, where introducing a new name like Foo would make it non-assignable to a same-shape Bar type, and viceversa.

There’s this long standing issue tracking nominal typings additions to TS.

One common approximation of opaque types in TS is using a unique tag to make any two types structurally different:

// opaque type module:
export type EUR = { readonly _tag: 'EUR' };
export function eur(value: number): EUR {
  return value as any;
}
export function addEuros(a: EUR, b: EUR): EUR {
  return ((a as any) + (b as any)) as any;
}

// usage from other modules:
const result: EUR = addEuros(eur(1), eur(10)); // OK
const c = eur(1) + eur(10) // Error: Operator '+' cannot be applied to types 'EUR' and 'EUR'.

Even better, the tag can be encoded with a unique Symbol to make sure it is never accessed and used otherwise:

declare const tag: unique symbol;
export type EUR = { readonly [tag]: 'EUR' };

Note that these representation don’t have any effect at runtime, the only overhead is calling the eur constructor.

newtype-ts provides generic utilities for defining and using values of types that behave similar to my examples above.

Branded types

Another typical use case is to keep the non-assignability only in one direction, i.e. deal with an EUR type which is assignable to number:

declare const a: EUR;
const b: number = a; // OK

This can be obtained via so called “branded types”:

declare const tag: unique symbol
export type EUR = number & { readonly [tag]: 'EUR' };

See for instance this usage in the io-ts library.

Leave a Comment