1 | /**
|
---|
2 | Create an opaque type, which hides its internal details from the public, and can only be created by being used explicitly.
|
---|
3 |
|
---|
4 | The generic type parameter can be anything. It doesn't have to be an object.
|
---|
5 |
|
---|
6 | [Read more about opaque types.](https://codemix.com/opaque-types-in-javascript/)
|
---|
7 |
|
---|
8 | There have been several discussions about adding this feature to TypeScript via the `opaque type` operator, similar to how Flow does it. Unfortunately, nothing has (yet) moved forward:
|
---|
9 | - [Microsoft/TypeScript#15408](https://github.com/Microsoft/TypeScript/issues/15408)
|
---|
10 | - [Microsoft/TypeScript#15807](https://github.com/Microsoft/TypeScript/issues/15807)
|
---|
11 |
|
---|
12 | @example
|
---|
13 | ```
|
---|
14 | import {Opaque} from 'type-fest';
|
---|
15 |
|
---|
16 | type AccountNumber = Opaque<number, 'AccountNumber'>;
|
---|
17 | type AccountBalance = Opaque<number, 'AccountBalance'>;
|
---|
18 |
|
---|
19 | // The Token parameter allows the compiler to differentiate between types, whereas "unknown" will not. For example, consider the following structures:
|
---|
20 | type ThingOne = Opaque<string>;
|
---|
21 | type ThingTwo = Opaque<string>;
|
---|
22 |
|
---|
23 | // To the compiler, these types are allowed to be cast to each other as they have the same underlying type. They are both `string & { __opaque__: unknown }`.
|
---|
24 | // To avoid this behaviour, you would instead pass the "Token" parameter, like so.
|
---|
25 | type NewThingOne = Opaque<string, 'ThingOne'>;
|
---|
26 | type NewThingTwo = Opaque<string, 'ThingTwo'>;
|
---|
27 |
|
---|
28 | // Now they're completely separate types, so the following will fail to compile.
|
---|
29 | function createNewThingOne (): NewThingOne {
|
---|
30 | // As you can see, casting from a string is still allowed. However, you may not cast NewThingOne to NewThingTwo, and vice versa.
|
---|
31 | return 'new thing one' as NewThingOne;
|
---|
32 | }
|
---|
33 |
|
---|
34 | // This will fail to compile, as they are fundamentally different types.
|
---|
35 | const thingTwo = createNewThingOne() as NewThingTwo;
|
---|
36 |
|
---|
37 | // Here's another example of opaque typing.
|
---|
38 | function createAccountNumber(): AccountNumber {
|
---|
39 | return 2 as AccountNumber;
|
---|
40 | }
|
---|
41 |
|
---|
42 | function getMoneyForAccount(accountNumber: AccountNumber): AccountBalance {
|
---|
43 | return 4 as AccountBalance;
|
---|
44 | }
|
---|
45 |
|
---|
46 | // This will compile successfully.
|
---|
47 | getMoneyForAccount(createAccountNumber());
|
---|
48 |
|
---|
49 | // But this won't, because it has to be explicitly passed as an `AccountNumber` type.
|
---|
50 | getMoneyForAccount(2);
|
---|
51 |
|
---|
52 | // You can use opaque values like they aren't opaque too.
|
---|
53 | const accountNumber = createAccountNumber();
|
---|
54 |
|
---|
55 | // This will not compile successfully.
|
---|
56 | const newAccountNumber = accountNumber + 2;
|
---|
57 |
|
---|
58 | // As a side note, you can (and should) use recursive types for your opaque types to make them stronger and hopefully easier to type.
|
---|
59 | type Person = {
|
---|
60 | id: Opaque<number, Person>;
|
---|
61 | name: string;
|
---|
62 | };
|
---|
63 | ```
|
---|
64 | */
|
---|
65 | export type Opaque<Type, Token = unknown> = Type & {readonly __opaque__: Token};
|
---|