diff --git a/index.d.ts b/index.d.ts index f5ddf294a..bcce1e75d 100644 --- a/index.d.ts +++ b/index.d.ts @@ -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'; diff --git a/readme.md b/readme.md index f22746a6f..b484871db 100644 --- a/readme.md +++ b/readme.md @@ -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`](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. diff --git a/source/opaque.d.ts b/source/opaque.d.ts index 9ed0fd9ca..aafbca55a 100644 --- a/source/opaque.d.ts +++ b/source/opaque.d.ts @@ -72,3 +72,36 @@ type Person = { @category Type */ export type Opaque = Type & Tagged; + +/** +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, 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; +``` + +@category Type +*/ +export type UnwrapOpaque> = + OpaqueType extends Opaque + ? Type + : OpaqueType; diff --git a/test-d/opaque.ts b/test-d/opaque.ts index 5def8c7e3..e8997054a 100644 --- a/test-d/opaque.ts +++ b/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; @@ -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(userJohn); + +// Remove tag from opaque value. +// Note: This will simply return number as type. +type PlainValue = UnwrapOpaque; +expectAssignable(123); + +const plainValue: PlainValue = 123 as PlainValue; +expectNotType(plainValue);