Skip to content

Commit

Permalink
Add UnwrapOpaque type (#403)
Browse files Browse the repository at this point in the history
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
  • Loading branch information
jmike and sindresorhus committed Jun 24, 2022
1 parent 5aa7dbd commit d4d4481
Show file tree
Hide file tree
Showing 4 changed files with 45 additions and 3 deletions.
2 changes: 1 addition & 1 deletion index.d.ts
Expand Up @@ -18,7 +18,7 @@ export {PartialDeep} from './source/partial-deep';
export {ReadonlyDeep} from './source/readonly-deep';
export {LiteralUnion} from './source/literal-union';
export {Promisable} from './source/promisable';
export {Opaque} from './source/opaque';
export {Opaque, UnwrapOpaque} from './source/opaque';
export {InvariantOf} from './source/invariant-of';
export {SetOptional} from './source/set-optional';
export {SetRequired} from './source/set-required';
Expand Down
1 change: 1 addition & 0 deletions readme.md
Expand Up @@ -172,6 +172,7 @@ Click the type names for complete docs.
- [`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.
- [`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).
- [`Opaque`](source/opaque.d.ts) - Create an [opaque type](https://codemix.com/opaque-types-in-javascript/).
- [`UnwrapOpaque`](source/opaque.d.ts) - Revert an [opaque type](https://codemix.com/opaque-types-in-javascript/) back to its original type.
- [`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.
- [`SetOptional`](source/set-optional.d.ts) - Create a type that makes the given keys optional.
- [`SetRequired`](source/set-required.d.ts) - Create a type that makes the given keys required.
Expand Down
33 changes: 33 additions & 0 deletions source/opaque.d.ts
Expand Up @@ -72,3 +72,36 @@ type Person = {
@category Type
*/
export type Opaque<Type, Token = unknown> = Type & Tagged<Token>;

/**
Revert an opaque 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>;
```
@category Type
*/
export type UnwrapOpaque<OpaqueType extends Tagged<unknown>> =
OpaqueType extends Opaque<infer Type, OpaqueType[typeof tag]>
? Type
: OpaqueType;
12 changes: 10 additions & 2 deletions test-d/opaque.ts
@@ -1,5 +1,5 @@
import {expectAssignable, expectError} from 'tsd';
import type {Opaque} from '../index';
import {expectAssignable, expectError, expectNotType} from 'tsd';
import type {Opaque, UnwrapOpaque} from '../index';

type Value = Opaque<number, 'Value'>;

Expand Down Expand Up @@ -38,3 +38,11 @@ const johnsId = '7dd4a16e-d5ee-454c-b1d0-71e23d9fa70b' as UUID;
// @ts-expect-error
const userJohn = userEntities[johnsId]; // eslint-disable-line @typescript-eslint/no-unused-vars
/// expectType<Foo>(userJohn);

// Remove tag from opaque value.
// Note: This will simply return number as type.
type PlainValue = UnwrapOpaque<Value>;
expectAssignable<PlainValue>(123);

const plainValue: PlainValue = 123 as PlainValue;
expectNotType<Value>(plainValue);

0 comments on commit d4d4481

Please sign in to comment.