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?
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.
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.
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With