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.