UNPKG

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