Skip to content

Commit 531414d

Browse files
committed
cleanup: remove Opaque and UnwrapOpaque
1 parent fb6c2aa commit 531414d

9 files changed

+48
-201
lines changed

index.d.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export type {UndefinedOnPartialDeep} from './source/undefined-on-partial-deep';
3636
export type {ReadonlyDeep} from './source/readonly-deep';
3737
export type {LiteralUnion} from './source/literal-union';
3838
export type {Promisable} from './source/promisable';
39-
export type {Opaque, UnwrapOpaque, Tagged, GetTagMetadata, UnwrapTagged} from './source/opaque';
39+
export type {Tagged, GetTagMetadata, UnwrapTagged} from './source/tagged';
4040
export type {InvariantOf} from './source/invariant-of';
4141
export type {SetOptional} from './source/set-optional';
4242
export type {SetReadonly} from './source/set-readonly';

readme.md

+4-4
Original file line numberDiff line numberDiff line change
@@ -151,8 +151,8 @@ Click the type names for complete docs.
151151
- [`UndefinedOnPartialDeep`](source/undefined-on-partial-deep.d.ts) - Create a deep version of another type where all optional keys are set to also accept `undefined`.
152152
- [`ReadonlyDeep`](source/readonly-deep.d.ts) - Create a deeply immutable version of an `object`/`Map`/`Set`/`Array` type. Use [`Readonly<T>`](https://www.typescriptlang.org/docs/handbook/utility-types.html#readonlytype) if you only need one level deep.
153153
- [`LiteralUnion`](source/literal-union.d.ts) - Create a union type by combining primitive types and literal types without sacrificing auto-completion in IDEs for the literal type part of the union. Workaround for [Microsoft/TypeScript#29729](https://github.com/Microsoft/TypeScript/issues/29729).
154-
- [`Tagged`](source/opaque.d.ts) - Create a [tagged type](https://medium.com/@KevinBGreene/surviving-the-typescript-ecosystem-branding-and-type-tagging-6cf6e516523d) that can support [multiple tags](https://github.com/sindresorhus/type-fest/issues/665) and [per-tag metadata](https://medium.com/@ethanresnick/advanced-typescript-tagged-types-improved-with-type-level-metadata-5072fc125fcf). (This replaces the previous [`Opaque`](source/opaque.d.ts) type, which is now deprecated.)
155-
- [`UnwrapTagged`](source/opaque.d.ts) - Get the untagged portion of a tagged type created with `Tagged`. (This replaces the previous [`UnwrapOpaque`](source/opaque.d.ts) type, which is now deprecated.)
154+
- [`Tagged`](source/tagged.d.ts) - Create a [tagged type](https://medium.com/@KevinBGreene/surviving-the-typescript-ecosystem-branding-and-type-tagging-6cf6e516523d) that can support [multiple tags](https://github.com/sindresorhus/type-fest/issues/665) and [per-tag metadata](https://medium.com/@ethanresnick/advanced-typescript-tagged-types-improved-with-type-level-metadata-5072fc125fcf).
155+
- [`UnwrapTagged`](source/tagged.d.ts) - Get the untagged portion of a tagged type created with `Tagged`.
156156
- [`InvariantOf`](source/invariant-of.d.ts) - Create an [invariant type](https://basarat.gitbook.io/typescript/type-system/type-compatibility#footnote-invariance), which is a type that does not accept supertypes and subtypes.
157157
- [`SetOptional`](source/set-optional.d.ts) - Create a type that makes the given keys optional.
158158
- [`SetReadonly`](source/set-readonly.d.ts) - Create a type that makes the given keys readonly.
@@ -345,8 +345,8 @@ type ShouldBeNever = IfAny<'not any', 'not never', 'never'>;
345345
- `RequireOnlyOne` - See [`RequireExactlyOne`](source/require-exactly-one.d.ts)
346346
- `AtMostOne` - See [`RequireOneOrNone`](source/require-one-or-none.d.ts)
347347
- `AllKeys` - See [`KeysOfUnion`](source/keys-of-union.d.ts)
348-
- `Branded` - See [`Tagged`](source/opaque.d.ts)
349-
- `Opaque` - See [`Tagged`](source/opaque.d.ts)
348+
- `Branded` - See [`Tagged`](source/tagged.d.ts)
349+
- `Opaque` - See [`Tagged`](source/tagged.d.ts)
350350

351351
## Tips
352352

source/exact.d.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type {ArrayElement, ObjectValue} from './internal';
2-
import type {Opaque, TagContainer} from './opaque';
2+
import type {TagContainer} from './tagged';
33
import type {IsEqual} from './is-equal';
44
import type {KeysOfUnion} from './keys-of-union';
55

source/invariant-of.d.ts

-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import type {Opaque} from './opaque';
2-
31
declare const invariantBrand: unique symbol;
42

53
/**

source/opaque.d.ts renamed to source/tagged.d.ts

-113
Original file line numberDiff line numberDiff line change
@@ -6,119 +6,6 @@ export type TagContainer<Token> = {
66

77
type Tag<Token extends PropertyKey, TagMetadata> = TagContainer<{[K in Token]: TagMetadata}>;
88

9-
/**
10-
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.)
11-
12-
The generic type parameters can be anything.
13-
14-
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.)
15-
16-
Also note that this implementation is limited to a single tag. If you want to allow multiple tags, use `Tagged` instead.
17-
18-
[Read more about tagged types.](https://medium.com/@KevinBGreene/surviving-the-typescript-ecosystem-branding-and-type-tagging-6cf6e516523d)
19-
20-
There have been several discussions about adding similar features to TypeScript. Unfortunately, nothing has (yet) moved forward:
21-
- [Microsoft/TypeScript#202](https://github.com/microsoft/TypeScript/issues/202)
22-
- [Microsoft/TypeScript#15408](https://github.com/Microsoft/TypeScript/issues/15408)
23-
- [Microsoft/TypeScript#15807](https://github.com/Microsoft/TypeScript/issues/15807)
24-
25-
@example
26-
```
27-
import type {Opaque} from 'type-fest';
28-
29-
type AccountNumber = Opaque<number, 'AccountNumber'>;
30-
type AccountBalance = Opaque<number, 'AccountBalance'>;
31-
32-
// The `Token` parameter allows the compiler to differentiate between types, whereas "unknown" will not. For example, consider the following structures:
33-
type ThingOne = Opaque<string>;
34-
type ThingTwo = Opaque<string>;
35-
36-
// 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 }`.
37-
// To avoid this behaviour, you would instead pass the "Token" parameter, like so.
38-
type NewThingOne = Opaque<string, 'ThingOne'>;
39-
type NewThingTwo = Opaque<string, 'ThingTwo'>;
40-
41-
// Now they're completely separate types, so the following will fail to compile.
42-
function createNewThingOne (): NewThingOne {
43-
// As you can see, casting from a string is still allowed. However, you may not cast NewThingOne to NewThingTwo, and vice versa.
44-
return 'new thing one' as NewThingOne;
45-
}
46-
47-
// This will fail to compile, as they are fundamentally different types.
48-
const thingTwo = createNewThingOne() as NewThingTwo;
49-
50-
// Here's another example of opaque typing.
51-
function createAccountNumber(): AccountNumber {
52-
return 2 as AccountNumber;
53-
}
54-
55-
function getMoneyForAccount(accountNumber: AccountNumber): AccountBalance {
56-
return 4 as AccountBalance;
57-
}
58-
59-
// This will compile successfully.
60-
getMoneyForAccount(createAccountNumber());
61-
62-
// But this won't, because it has to be explicitly passed as an `AccountNumber` type.
63-
getMoneyForAccount(2);
64-
65-
// You can use opaque values like they aren't opaque too.
66-
const accountNumber = createAccountNumber();
67-
68-
// This will compile successfully.
69-
const newAccountNumber = accountNumber + 2;
70-
71-
// As a side note, you can (and should) use recursive types for your opaque types to make them stronger and hopefully easier to type.
72-
type Person = {
73-
id: Opaque<number, Person>;
74-
name: string;
75-
};
76-
```
77-
78-
@category Type
79-
@deprecated Use {@link Tagged} instead
80-
*/
81-
export type Opaque<Type, Token = unknown> = Type & TagContainer<Token>;
82-
83-
/**
84-
Revert an opaque or tagged type back to its original type by removing the readonly `[tag]`.
85-
86-
Why is this necessary?
87-
88-
1. Use an `Opaque` type as object keys
89-
2. Prevent TS4058 error: "Return type of exported function has or is using name X from external module Y but cannot be named"
90-
91-
@example
92-
```
93-
import type {Opaque, UnwrapOpaque} from 'type-fest';
94-
95-
type AccountType = Opaque<'SAVINGS' | 'CHECKING', 'AccountType'>;
96-
97-
const moneyByAccountType: Record<UnwrapOpaque<AccountType>, number> = {
98-
SAVINGS: 99,
99-
CHECKING: 0.1
100-
};
101-
102-
// Without UnwrapOpaque, the following expression would throw a type error.
103-
const money = moneyByAccountType.SAVINGS; // TS error: Property 'SAVINGS' does not exist
104-
105-
// Attempting to pass an non-Opaque type to UnwrapOpaque will raise a type error.
106-
type WontWork = UnwrapOpaque<string>;
107-
108-
// Using a Tagged type will work too.
109-
type WillWork = UnwrapOpaque<Tagged<number, 'AccountNumber'>>; // number
110-
```
111-
112-
@category Type
113-
@deprecated Use {@link UnwrapTagged} instead
114-
*/
115-
export type UnwrapOpaque<OpaqueType extends TagContainer<unknown>> =
116-
OpaqueType extends Tag<PropertyKey, any>
117-
? RemoveAllTags<OpaqueType>
118-
: OpaqueType extends Opaque<infer Type, OpaqueType[typeof tag]>
119-
? Type
120-
: OpaqueType;
121-
1229
/**
12310
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.)
12411

test-d/exact.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type {Exact, Opaque} from '../index';
1+
import type {Exact, Tagged} from '../index';
22

33
{ // Spec - string type
44
type Type = string;
@@ -356,7 +356,7 @@ import type {Exact, Opaque} from '../index';
356356
// Spec - special test case for Opaque types
357357
// @see https://github.com/sindresorhus/type-fest/issues/508
358358
{
359-
type SpecialName = Opaque<string, 'special name'>;
359+
type SpecialName = Tagged<string, 'special name'>;
360360

361361
type OnlyAcceptName = {
362362
name: SpecialName;
@@ -375,7 +375,7 @@ import type {Exact, Opaque} from '../index';
375375
// @see https://github.com/sindresorhus/type-fest/issues/508
376376
{
377377
// Test for number Opaque type
378-
type SpecialName = Opaque<number, 'special name'>;
378+
type SpecialName = Tagged<number, 'special name'>;
379379

380380
type OnlyAcceptName = {
381381
name: SpecialName;
@@ -392,7 +392,7 @@ import type {Exact, Opaque} from '../index';
392392

393393
// Spec - test the above for tagged types too.
394394
{
395-
type TaggedNumber = Opaque<number, 'tag'>;
395+
type TaggedNumber = Tagged<number, 'tag'>;
396396

397397
const function_ = <T extends Exact<{a: TaggedNumber}, T>>(arguments_: T) => arguments_;
398398

test-d/readonly-deep.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {expectType, expectAssignable} from 'tsd';
2-
import type {Opaque, tag} from '../source/opaque';
2+
import type {Tagged, tag} from '../source/tagged';
33
import type {ReadonlyDeep, ReadonlyObjectDeep} from '../source/readonly-deep';
44
import type {JsonValue} from '../source/basic';
55

@@ -17,8 +17,8 @@ type NamespaceWithOverload = Overloaded & {
1717
baz: boolean[];
1818
};
1919

20-
type OpaqueObjectData = {a: number[]} | {b: string};
21-
type OpaqueObject = Opaque<OpaqueObjectData, {token: unknown}>;
20+
type TaggedObjectData = {a: number[]} | {b: string};
21+
type TaggedObject = Tagged<TaggedObjectData, 'token', unknown>;
2222

2323
type ReadonlyJsonValue =
2424
| {readonly [k: string]: ReadonlyJsonValue}
@@ -62,7 +62,7 @@ const data = {
6262
readonlyArray: ['foo'] as readonly string[],
6363
readonlyTuple: ['foo'] as const,
6464
json: [{x: true}] as JsonValue,
65-
opaqueObj: {a: [3]} as OpaqueObject, // eslint-disable-line @typescript-eslint/consistent-type-assertions
65+
opaqueObj: {a: [3]} as TaggedObject, // eslint-disable-line @typescript-eslint/consistent-type-assertions
6666
};
6767

6868
const readonlyData: ReadonlyDeep<typeof data> = data;
@@ -99,7 +99,7 @@ expectType<Readonly<ReadonlySet<string>>>(readonlyData.readonlySet);
9999
expectType<readonly string[]>(readonlyData.readonlyArray);
100100
expectType<readonly ['foo']>(readonlyData.readonlyTuple);
101101
expectAssignable<ReadonlyJsonValue>(readonlyData.json);
102-
expectAssignable<Opaque<ReadonlyDeep<OpaqueObjectData>, ReadonlyDeep<OpaqueObject[typeof tag]>>>(readonlyData.opaqueObj);
102+
expectAssignable<ReadonlyDeep<TaggedObjectData> & ReadonlyDeep<{[tag]: TaggedObject[typeof tag]}>>(readonlyData.opaqueObj);
103103

104104
expectType<((foo: number) => string) & ReadonlyObjectDeep<Namespace>>(readonlyData.namespace);
105105
expectType<string>(readonlyData.namespace(1));
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,34 @@
11
import {expectAssignable, expectNotAssignable, expectNotType, expectType} from 'tsd';
2-
import type {Opaque, UnwrapOpaque, Tagged, GetTagMetadata, UnwrapTagged, InvariantOf, SnakeCasedPropertiesDeep} from '../index';
2+
import type {Tagged, GetTagMetadata, UnwrapTagged, InvariantOf, SnakeCasedPropertiesDeep} from '../index';
33

4-
type Value = Opaque<number, 'Value'>;
4+
type TaggedValue = Tagged<number, 'Value'>;
55

66
// We make an explicit cast so we can test the value.
7-
const value: Value = 2 as Value;
7+
const value = 2 as TaggedValue;
88

99
// The underlying type of the value is still a number.
1010
expectAssignable<number>(value);
1111

12-
// You cannot modify an opaque value (and still get back an opaque value).
13-
expectNotAssignable<Value>(value + 2);
12+
// If you modify a tagged value, the result is not still tagged.
13+
expectNotAssignable<TaggedValue>(value + 2);
1414

1515
// But you can modify one if you're just treating it as its underlying type.
1616
expectAssignable<number>(value + 2);
1717

18-
type WithoutToken = Opaque<number>;
19-
expectAssignable<WithoutToken>(2 as WithoutToken);
20-
21-
// Verify that the Opaque's token can be the parent type itself.
18+
// Verify that a tag's medatadata can be the parent type itself.
2219
type Person = {
23-
id: Opaque<number, Person>;
20+
id: Tagged<number, 'PersonId', Person>;
2421
name: string;
2522
};
2623
const person = {
27-
id: 42 as Opaque<number, Person>,
24+
id: 42 as Tagged<number, 'PersonId', Person>,
2825
name: 'Arthur',
2926
};
3027
expectType<Person>(person);
3128

3229
// Failing test for https://github.com/sindresorhus/type-fest/issues/108
33-
// Use `Opaque` value as `Record` index type.
34-
type UUID = Opaque<string, 'UUID'>;
30+
// Use `Tagged` value as `Record` index type.
31+
type UUID = Tagged<string, 'UUID'>;
3532
type NormalizedDictionary<T> = Record<UUID, T>;
3633
type Foo = {bar: string};
3734

@@ -45,41 +42,6 @@ const johnsId = '7dd4a16e-d5ee-454c-b1d0-71e23d9fa70b' as UUID;
4542
const userJohn = userEntities[johnsId];
4643
expectType<Foo>(userJohn);
4744

48-
// Remove tag from opaque value.
49-
// Note: This will simply return number as type.
50-
type PlainValue = UnwrapOpaque<Value>;
51-
expectAssignable<PlainValue>(123);
52-
53-
const plainValue: PlainValue = 123 as PlainValue;
54-
expectNotType<Value>(plainValue);
55-
56-
// UnwrapOpque should work even when the token _happens_ to make the Opaque type
57-
// have the same underlying structure as a Tagged type.
58-
expectType<number>(4 as UnwrapOpaque<Opaque<number, {x: void}>>);
59-
60-
// All the basic tests that apply to Opaque types should pass for Tagged types too.
61-
// See rationale for each test in the Opaque tests above.
62-
//
63-
// Tests around not providing a token, which Tagged requires, or using non-
64-
// `string | number | symbol` tags, which Tagged doesn't support, are excluded.
65-
type TaggedValue = Tagged<number, 'Value'>;
66-
type TaggedUUID = Tagged<string, 'UUID'>;
67-
68-
const taggedValue: TaggedValue = 2 as TaggedValue;
69-
expectAssignable<number>(taggedValue);
70-
expectNotAssignable<TaggedValue>(value + 2);
71-
expectAssignable<number>(value + 2);
72-
73-
const userEntities2: Record<TaggedUUID, Foo> = {
74-
['7dd4a16e-d5ee-454c-b1d0-71e23d9fa70b' as UUID]: {bar: 'John'},
75-
['6ce31270-31eb-4a72-a9bf-43192d4ab436' as UUID]: {bar: 'Doe'},
76-
};
77-
78-
const johnsId2 = '7dd4a16e-d5ee-454c-b1d0-71e23d9fa70b' as TaggedUUID;
79-
80-
const userJohn2 = userEntities2[johnsId2];
81-
expectType<Foo>(userJohn2);
82-
8345
// Tagged types should support multiple tags,
8446
// by intersection or repeated application of Tagged.
8547
type AbsolutePath = Tagged<string, 'AbsolutePath'>;
@@ -97,27 +59,27 @@ expectAssignable<NormalizedPath>('' as NormalizedAbsolutePath);
9759
expectNotAssignable<SpecialCacheKey>('' as UrlString);
9860
expectAssignable<UrlString>('' as SpecialCacheKey);
9961

100-
// A tag that is a union type should be treated as multiple tags.
101-
// This is the only practical-to-implement behavior, given how we're storing the tags.
102-
// However, it's also arguably the desirable behavior, and it's what the TS team planned to implement:
103-
// https://github.com/microsoft/TypeScript/pull/33290#issuecomment-529710519
104-
expectAssignable<Tagged<number, 'Y'>>(4 as Tagged<number, 'X' | 'Y'>);
105-
106-
// UnwrapOpaque and UnwrapTagged both work on Tagged types.
107-
type PlainValueUnwrapOpaque = UnwrapOpaque<TaggedValue>;
108-
type PlainValueUnwrapTagged = UnwrapTagged<TaggedValue>;
109-
110-
const unwrapped1 = 123 as PlainValueUnwrapOpaque;
111-
const unwrapped2 = 123 as PlainValueUnwrapTagged;
62+
// Remove tag from tagged value. UnwrapTagged should work on types with multiple tags.
63+
type PlainValue = UnwrapTagged<TaggedValue>;
64+
expectAssignable<PlainValue>(123);
11265

66+
const unwrapped1 = 123 as UnwrapTagged<TaggedValue>;
67+
const unwrapped2 = '' as UnwrapTagged<NormalizedAbsolutePath>;
11368
expectType<number>(unwrapped1);
114-
expectType<number>(unwrapped2);
69+
expectType<string>(unwrapped2);
11570

116-
// UnwrapTagged/UnwrapOpaque should work on types with multiple tags.
71+
const plainValue: PlainValue = 123 as PlainValue;
72+
expectNotType<TaggedValue>(plainValue);
73+
74+
// UnwrapTagged should work on types with multiple tags.
11775
const unwrapped3 = '' as UnwrapTagged<NormalizedAbsolutePath>;
118-
const unwrapped4 = '' as UnwrapOpaque<NormalizedAbsolutePath>;
11976
expectType<string>(unwrapped3);
120-
expectType<string>(unwrapped4);
77+
78+
// A tag that is a union type should be treated as multiple tags.
79+
// This is the only practical-to-implement behavior, given how we're storing the tags.
80+
// However, it's also arguably the desirable behavior, and it's what the TS team planned to implement:
81+
// https://github.com/microsoft/TypeScript/pull/33290#issuecomment-529710519
82+
expectAssignable<Tagged<number, 'Y'>>(4 as Tagged<number, 'X' | 'Y'>);
12183

12284
// Tags have no metadata by default
12385
expectType<never>(undefined as unknown as GetTagMetadata<UrlString, 'URL'>);
@@ -143,6 +105,6 @@ expectAssignable<JsonOf<InvariantOf<number>>>(
143105
);
144106

145107
// Test for issue https://github.com/sindresorhus/type-fest/issues/643
146-
type IdType = Opaque<number, 'test'>;
108+
type IdType = Tagged<number, 'test'>;
147109
type TestSnakeObject = SnakeCasedPropertiesDeep<{testId: IdType}>;
148110
expectType<TestSnakeObject>({test_id: 2 as IdType});

0 commit comments

Comments
 (0)