forked from sindresorhus/type-fest
-
Notifications
You must be signed in to change notification settings - Fork 0
/
opaque.d.ts
252 lines (184 loc) · 10.1 KB
/
opaque.d.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
declare const tag: unique symbol;
export type TagContainer<Token> = {
readonly [tag]: Token;
};
type Tag<Token extends PropertyKey, TagMetadata> = TagContainer<{[K in Token]: TagMetadata}>;
/**
Attach a "tag" to an arbitrary type. This allows you to create distinct types, that aren't assignable to one another, for runtime values that would otherwise have the same type. (See examples.)
The generic type parameters can be anything.
Note that `Opaque` is somewhat of a misnomer here, in that, unlike [some alternative implementations](https://github.com/microsoft/TypeScript/issues/4895#issuecomment-425132582), the original, untagged type is not actually hidden. (E.g., functions that accept the untagged type can still be called with the "opaque" version -- but not vice-versa.)
Also note that this implementation is limited to a single tag. If you want to allow multiple tags, use `Tagged` instead.
[Read more about tagged types.](https://medium.com/@KevinBGreene/surviving-the-typescript-ecosystem-branding-and-type-tagging-6cf6e516523d)
There have been several discussions about adding similar features to TypeScript. Unfortunately, nothing has (yet) moved forward:
- [Microsoft/TypeScript#202](https://github.com/microsoft/TypeScript/issues/202)
- [Microsoft/TypeScript#15408](https://github.com/Microsoft/TypeScript/issues/15408)
- [Microsoft/TypeScript#15807](https://github.com/Microsoft/TypeScript/issues/15807)
@example
```
import type {Opaque} from 'type-fest';
type AccountNumber = Opaque<number, 'AccountNumber'>;
type AccountBalance = Opaque<number, 'AccountBalance'>;
// The `Token` parameter allows the compiler to differentiate between types, whereas "unknown" will not. For example, consider the following structures:
type ThingOne = Opaque<string>;
type ThingTwo = Opaque<string>;
// 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 }`.
// To avoid this behaviour, you would instead pass the "Token" parameter, like so.
type NewThingOne = Opaque<string, 'ThingOne'>;
type NewThingTwo = Opaque<string, 'ThingTwo'>;
// Now they're completely separate types, so the following will fail to compile.
function createNewThingOne (): NewThingOne {
// As you can see, casting from a string is still allowed. However, you may not cast NewThingOne to NewThingTwo, and vice versa.
return 'new thing one' as NewThingOne;
}
// This will fail to compile, as they are fundamentally different types.
const thingTwo = createNewThingOne() as NewThingTwo;
// Here's another example of opaque typing.
function createAccountNumber(): AccountNumber {
return 2 as AccountNumber;
}
function getMoneyForAccount(accountNumber: AccountNumber): AccountBalance {
return 4 as AccountBalance;
}
// This will compile successfully.
getMoneyForAccount(createAccountNumber());
// But this won't, because it has to be explicitly passed as an `AccountNumber` type.
getMoneyForAccount(2);
// You can use opaque values like they aren't opaque too.
const accountNumber = createAccountNumber();
// This will compile successfully.
const newAccountNumber = accountNumber + 2;
// As a side note, you can (and should) use recursive types for your opaque types to make them stronger and hopefully easier to type.
type Person = {
id: Opaque<number, Person>;
name: string;
};
```
@category Type
@deprecated Use {@link Tagged} instead
*/
export type Opaque<Type, Token = unknown> = Type & TagContainer<Token>;
/**
Revert an opaque or tagged type back to its original type by removing the readonly `[tag]`.
Why is this necessary?
1. Use an `Opaque` type as object keys
2. Prevent TS4058 error: "Return type of exported function has or is using name X from external module Y but cannot be named"
@example
```
import type {Opaque, UnwrapOpaque} from 'type-fest';
type AccountType = Opaque<'SAVINGS' | 'CHECKING', 'AccountType'>;
const moneyByAccountType: Record<UnwrapOpaque<AccountType>, number> = {
SAVINGS: 99,
CHECKING: 0.1
};
// Without UnwrapOpaque, the following expression would throw a type error.
const money = moneyByAccountType.SAVINGS; // TS error: Property 'SAVINGS' does not exist
// Attempting to pass an non-Opaque type to UnwrapOpaque will raise a type error.
type WontWork = UnwrapOpaque<string>;
// Using a Tagged type will work too.
type WillWork = UnwrapOpaque<Tagged<number, 'AccountNumber'>>; // number
```
@category Type
@deprecated Use {@link UnwrapTagged} instead
*/
export type UnwrapOpaque<OpaqueType extends TagContainer<unknown>> =
OpaqueType extends Tag<PropertyKey, any>
? RemoveAllTags<OpaqueType>
: OpaqueType extends Opaque<infer Type, OpaqueType[typeof tag]>
? Type
: OpaqueType;
/**
Attach a "tag" to an arbitrary type. This allows you to create distinct types, that aren't assignable to one another, for distinct concepts in your program that should not be interchangeable, even if their runtime values have the same type. (See examples.)
A type returned by `Tagged` can be passed to `Tagged` again, to create a type with multiple tags.
[Read more about tagged types.](https://medium.com/@KevinBGreene/surviving-the-typescript-ecosystem-branding-and-type-tagging-6cf6e516523d)
A tag's name is usually a string (and must be a string, number, or symbol), but each application of a tag can also contain an arbitrary type as its "metadata". See {@link GetTagMetadata} for examples and explanation.
A type `A` returned by `Tagged` is assignable to another type `B` returned by `Tagged` if and only if:
- the underlying (untagged) type of `A` is assignable to the underlying type of `B`;
- `A` contains at least all the tags `B` has;
- and the metadata type for each of `A`'s tags is assignable to the metadata type of `B`'s corresponding tag.
There have been several discussions about adding similar features to TypeScript. Unfortunately, nothing has (yet) moved forward:
- [Microsoft/TypeScript#202](https://github.com/microsoft/TypeScript/issues/202)
- [Microsoft/TypeScript#4895](https://github.com/microsoft/TypeScript/issues/4895)
- [Microsoft/TypeScript#33290](https://github.com/microsoft/TypeScript/pull/33290)
@example
```
import type {Tagged} from 'type-fest';
type AccountNumber = Tagged<number, 'AccountNumber'>;
type AccountBalance = Tagged<number, 'AccountBalance'>;
function createAccountNumber(): AccountNumber {
// As you can see, casting from a `number` (the underlying type being tagged) is allowed.
return 2 as AccountNumber;
}
function getMoneyForAccount(accountNumber: AccountNumber): AccountBalance {
return 4 as AccountBalance;
}
// This will compile successfully.
getMoneyForAccount(createAccountNumber());
// But this won't, because it has to be explicitly passed as an `AccountNumber` type!
// Critically, you could not accidentally use an `AccountBalance` as an `AccountNumber`.
getMoneyForAccount(2);
// You can also use tagged values like their underlying, untagged type.
// I.e., this will compile successfully because an `AccountNumber` can be used as a regular `number`.
// In this sense, the underlying base type is not hidden, which differentiates tagged types from opaque types in other languages.
const accountNumber = createAccountNumber() + 2;
```
@example
```
import type {Tagged} from 'type-fest';
// You can apply multiple tags to a type by using `Tagged` repeatedly.
type Url = Tagged<string, 'URL'>;
type SpecialCacheKey = Tagged<Url, 'SpecialCacheKey'>;
// You can also pass a union of tag names, so this is equivalent to the above, although it doesn't give you the ability to assign distinct metadata to each tag.
type SpecialCacheKey2 = Tagged<string, 'URL' | 'SpecialCacheKey'>;
```
@category Type
*/
export type Tagged<Type, TagName extends PropertyKey, TagMetadata = never> = Type & Tag<TagName, TagMetadata>;
/**
Given a type and a tag name, returns the metadata associated with that tag on that type.
In the example below, one could use `Tagged<string, 'JSON'>` to represent "a string that is valid JSON". That type might be useful -- for instance, it communicates that the value can be safely passed to `JSON.parse` without it throwing an exception. However, it doesn't indicate what type of value will be produced on parse (which is sometimes known). `JsonOf<T>` solves this; it represents "a string that is valid JSON and that, if parsed, would produce a value of type T". The type T is held in the metadata associated with the `'JSON'` tag.
This article explains more about [how tag metadata works and when it can be useful](https://medium.com/@ethanresnick/advanced-typescript-tagged-types-improved-with-type-level-metadata-5072fc125fcf).
@example
```
import type {Tagged} from 'type-fest';
type JsonOf<T> = Tagged<string, 'JSON', T>;
function stringify<T>(it: T) {
return JSON.stringify(it) as JsonOf<T>;
}
function parse<T extends JsonOf<unknown>>(it: T) {
return JSON.parse(it) as GetTagMetadata<T, 'JSON'>;
}
const x = stringify({ hello: 'world' });
const parsed = parse(x); // The type of `parsed` is { hello: string }
```
@category Type
*/
export type GetTagMetadata<Type extends Tag<TagName, unknown>, TagName extends PropertyKey> = Type[typeof tag][TagName];
/**
Revert a tagged type back to its original type by removing all tags.
Why is this necessary?
1. Use a `Tagged` type as object keys
2. Prevent TS4058 error: "Return type of exported function has or is using name X from external module Y but cannot be named"
@example
```
import type {Tagged, UnwrapTagged} from 'type-fest';
type AccountType = Tagged<'SAVINGS' | 'CHECKING', 'AccountType'>;
const moneyByAccountType: Record<UnwrapTagged<AccountType>, number> = {
SAVINGS: 99,
CHECKING: 0.1
};
// Without UnwrapTagged, the following expression would throw a type error.
const money = moneyByAccountType.SAVINGS; // TS error: Property 'SAVINGS' does not exist
// Attempting to pass an non-Tagged type to UnwrapTagged will raise a type error.
type WontWork = UnwrapTagged<string>;
```
@category Type
*/
export type UnwrapTagged<TaggedType extends Tag<PropertyKey, any>> =
RemoveAllTags<TaggedType>;
type RemoveAllTags<T> = T extends Tag<PropertyKey, any>
? {
[ThisTag in keyof T[typeof tag]]: T extends Tagged<infer Type, ThisTag, T[typeof tag][ThisTag]>
? RemoveAllTags<Type>
: never
}[keyof T[typeof tag]]
: T;