Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to define an opaque type in TypeScript?

Tags:

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?

like image 528
ChrisW Avatar asked Jun 24 '19 12:06

ChrisW


People also ask

What is opaque type in Swift?

An opaque type refers to one specific type, although the caller of the function isn't able to see which type; a protocol type can refer to any type that conforms to the protocol.

What is symbol in TypeScript?

In Typescript symbol is a primitive data type. A primitive data type is not an object, it possesses no properties or methods and they cannot be altered. The symbol type is similar to other types such as number, string, boolean, etc. Symbol values are created using the Symbol constructor.


1 Answers

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.

like image 187
Giovanni Gonzaga Avatar answered Sep 19 '22 17:09

Giovanni Gonzaga