31

If I recall correctly, in C++ you can define an an opaque type like this ...

class Foo;

... and use it like a handle e.g. when declaring function signatures ...

void printFoo(const Foo& foo);

Application code might then work with references-to-Foo or pointers-to-Foo without seeing the actual definition of Foo.

Is there anything similar in TypeScript -- how would you define an opaque type?

My problem is that if I define this ...

interface Foo {};

... then that's freely interchangeable with other similar types. Is there an idiom?

ChrisW
  • 54,973
  • 13
  • 116
  • 224
  • Do you look about the Generic Type ? I am not sure to really understand your question but it may help you. – Benoit Chassignol Jun 24 '19 at 13:05
  • 2
    I think you are looking for branded types: https://stackoverflow.com/questions/49432350/how-to-represent-guid-in-typescript/49432424#49432424 – Titian Cernicova-Dragomir Jun 24 '19 at 13:13
  • @TitianCernicova-Dragomir Yes that's it. Would you like to answer this question or shall I delete it instead? – ChrisW Jun 24 '19 at 13:16
  • @ChrisW I don't think any of the other branded type questions reference opaque so it might be useful to add an answer to improve search-ability ... I'll add one once I'm out of a meeting – Titian Cernicova-Dragomir Jun 24 '19 at 13:20
  • Possible duplicate of [How to represent Guid in typescript?](https://stackoverflow.com/questions/49432350/how-to-represent-guid-in-typescript) – Paleo Jun 24 '19 at 14:20

1 Answers1

41

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.

chrisbajorin
  • 5,993
  • 3
  • 21
  • 34
Giovanni Gonzaga
  • 1,185
  • 9
  • 8
  • 5
    see also this great [blog post about Branding vs. Flavoring](https://spin.atomicobject.com/2018/01/15/typescript-flexible-nominal-typing/) – TmTron Feb 06 '20 at 19:07
  • 1
    A similar solution for branded types using enums : https://basarat.gitbook.io/typescript/main-1/nominaltyping – Pascal Pixel Rigaux Aug 08 '22 at 15:16