diff --git a/index.d.ts b/index.d.ts index 5c62cd009..d78b2a0aa 100644 --- a/index.d.ts +++ b/index.d.ts @@ -36,7 +36,7 @@ export type {UndefinedOnPartialDeep} from './source/undefined-on-partial-deep'; export type {ReadonlyDeep} from './source/readonly-deep'; export type {LiteralUnion} from './source/literal-union'; export type {Promisable} from './source/promisable'; -export type {Opaque, UnwrapOpaque, Tagged, GetTagMetadata, UnwrapTagged} from './source/opaque'; +export type {Tagged, GetTagMetadata, UnwrapTagged} from './source/tagged'; export type {InvariantOf} from './source/invariant-of'; export type {SetOptional} from './source/set-optional'; export type {SetReadonly} from './source/set-readonly'; diff --git a/readme.md b/readme.md index 899e24e45..d5f6a15d3 100644 --- a/readme.md +++ b/readme.md @@ -151,8 +151,8 @@ Click the type names for complete docs. - [`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`. - [`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). -- [`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) if needed 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.) -- [`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.) +- [`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) if needed and [per-tag metadata](https://medium.com/@ethanresnick/advanced-typescript-tagged-types-improved-with-type-level-metadata-5072fc125fcf). This replaces the former `Opaque` type. +- [`UnwrapTagged`](source/opaque.d.ts) - Get the untagged portion of a tagged type created with `Tagged`. This replaces the former `UnwrapOpaque` 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. - [`SetReadonly`](source/set-readonly.d.ts) - Create a type that makes the given keys readonly. diff --git a/source/exact.d.ts b/source/exact.d.ts index af54e9d46..4fee335f4 100644 --- a/source/exact.d.ts +++ b/source/exact.d.ts @@ -1,5 +1,5 @@ import type {ArrayElement, ObjectValue} from './internal'; -import type {Opaque, TagContainer} from './opaque'; +import type {TagContainer} from './tagged'; import type {IsEqual} from './is-equal'; import type {KeysOfUnion} from './keys-of-union'; diff --git a/source/invariant-of.d.ts b/source/invariant-of.d.ts index 2fa821e46..bb913ae77 100644 --- a/source/invariant-of.d.ts +++ b/source/invariant-of.d.ts @@ -1,5 +1,3 @@ -import type {Opaque} from './opaque'; - declare const invariantBrand: unique symbol; /** diff --git a/source/opaque.d.ts b/source/tagged.d.ts similarity index 56% rename from source/opaque.d.ts rename to source/tagged.d.ts index a517be1c1..f2071c2e2 100644 --- a/source/opaque.d.ts +++ b/source/tagged.d.ts @@ -6,119 +6,6 @@ export type TagContainer = { type Tag = 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; -type AccountBalance = Opaque; - -// The `Token` parameter allows the compiler to differentiate between types, whereas "unknown" will not. For example, consider the following structures: -type ThingOne = Opaque; -type ThingTwo = Opaque; - -// 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; -type NewThingTwo = Opaque; - -// 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; - name: string; -}; -``` - -@category Type -@deprecated Use {@link Tagged} instead -*/ -export type Opaque = Type & TagContainer; - -/** -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, 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; - -// Using a Tagged type will work too. -type WillWork = UnwrapOpaque>; // number -``` - -@category Type -@deprecated Use {@link UnwrapTagged} instead -*/ -export type UnwrapOpaque> = - OpaqueType extends Tag - ? RemoveAllTags - : OpaqueType extends Opaque - ? 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.) diff --git a/test-d/exact.ts b/test-d/exact.ts index bd504260b..689f41223 100644 --- a/test-d/exact.ts +++ b/test-d/exact.ts @@ -1,120 +1,142 @@ -import type {Exact, Opaque} from '../index'; +import type { Exact, Tagged } from "../index"; -{ // Spec - string type +{ + // Spec - string type type Type = string; const function_ = >(arguments_: T) => arguments_; - { // It should accept string - const input = ''; + { + // It should accept string + const input = ""; function_(input); } - { // It should reject number + { + // It should reject number const input = 1; // @ts-expect-error function_(input); } - { // It should reject object + { + // It should reject object const input = {}; // @ts-expect-error function_(input); } } -{ // Spec - array - type Type = Array<{code: string; name?: string}>; +{ + // Spec - array + type Type = Array<{ code: string; name?: string }>; const function_ = >(arguments_: T) => arguments_; - { // It should accept array with required property only - const input = [{code: ''}]; + { + // It should accept array with required property only + const input = [{ code: "" }]; function_(input); } - { // It should reject readonly array - const input = [{code: ''}] as ReadonlyArray<{code: string}>; + { + // It should reject readonly array + const input = [{ code: "" }] as ReadonlyArray<{ code: string }>; // @ts-expect-error function_(input); } - { // It should accept array with optional property - const input = [{code: '', name: ''}]; + { + // It should accept array with optional property + const input = [{ code: "", name: "" }]; function_(input); } - { // It should reject array with excess property - const input = [{code: '', name: '', excessProperty: ''}]; + { + // It should reject array with excess property + const input = [{ code: "", name: "", excessProperty: "" }]; // @ts-expect-error function_(input); } - { // It should reject invalid type - const input = ''; + { + // It should reject invalid type + const input = ""; // @ts-expect-error function_(input); } } -{ // Spec - readonly array - type Type = ReadonlyArray<{code: string; name?: string}>; +{ + // Spec - readonly array + type Type = ReadonlyArray<{ code: string; name?: string }>; const function_ = >(arguments_: T) => arguments_; - { // It should accept array with required property only - const input = [{code: ''}]; + { + // It should accept array with required property only + const input = [{ code: "" }]; function_(input); } - { // It should accept readonly array - const input = [{code: ''}] as ReadonlyArray<{code: string}>; + { + // It should accept readonly array + const input = [{ code: "" }] as ReadonlyArray<{ code: string }>; function_(input); } - { // It should accept array with optional property - const input = [{code: '', name: ''}]; + { + // It should accept array with optional property + const input = [{ code: "", name: "" }]; function_(input); } - { // It should reject array with excess property - const input = [{code: '', name: '', excessProperty: ''}]; + { + // It should reject array with excess property + const input = [{ code: "", name: "", excessProperty: "" }]; // @ts-expect-error function_(input); } - { // It should reject invalid type - const input = ''; + { + // It should reject invalid type + const input = ""; // @ts-expect-error function_(input); } } -{ // Spec - object - type Type = {code: string; name?: string}; +{ + // Spec - object + type Type = { code: string; name?: string }; const function_ = >(arguments_: T) => arguments_; - { // It should accept object with required property only - const input = {code: ''}; + { + // It should accept object with required property only + const input = { code: "" }; function_(input); } - { // It should accept object with optional property - const input = {code: '', name: ''}; + { + // It should accept object with optional property + const input = { code: "", name: "" }; function_(input); } - { // It should reject object with excess property - const input = {code: '', name: '', excessProperty: ''}; + { + // It should reject object with excess property + const input = { code: "", name: "", excessProperty: "" }; // @ts-expect-error function_(input); } - { // It should reject invalid type - const input = ''; + { + // It should reject invalid type + const input = ""; // @ts-expect-error function_(input); } } -{ // Spec - object with optional outer object @see https://github.com/sindresorhus/type-fest/pull/546#issuecomment-1385620838 +{ + // Spec - object with optional outer object @see https://github.com/sindresorhus/type-fest/pull/546#issuecomment-1385620838 type Type = { outer?: { inner: { @@ -126,55 +148,64 @@ import type {Exact, Opaque} from '../index'; function_({ outer: { inner: { - field: 'foo', + field: "foo", }, }, }); } -{ // Spec - union - only object - type Type = {code: string} | {name: string}; +{ + // Spec - union - only object + type Type = { code: string } | { name: string }; const function_ = >(arguments_: T) => arguments_; - { // It should accept type a - const input = {code: ''}; + { + // It should accept type a + const input = { code: "" }; function_(input); } - { // It should accept type b - const input = {name: ''}; + { + // It should accept type b + const input = { name: "" }; function_(input); } - { // It should reject intersection - const input = {name: '', code: ''}; + { + // It should reject intersection + const input = { name: "", code: "" }; // @ts-expect-error function_(input); } } -{ // Spec - union - mixture object/primitive - type Type = {code: string} | string; +{ + // Spec - union - mixture object/primitive + type Type = { code: string } | string; const function_ = >(arguments_: T) => arguments_; - { // It should accept type a - const input = {code: ''}; + { + // It should accept type a + const input = { code: "" }; function_(input); } - { // It should accept type b - const input = ''; + { + // It should accept type b + const input = ""; function_(input); } - { // It should reject intersection - const input = {name: '', code: ''}; + { + // It should reject intersection + const input = { name: "", code: "" }; // @ts-expect-error function_(input); } } -{ // Spec - jsonschema2ts generated request type with additionalProperties: true +{ + // Spec - jsonschema2ts generated request type with additionalProperties: true type Type = { body: { [k: string]: unknown; @@ -184,204 +215,256 @@ import type {Exact, Opaque} from '../index'; }; const function_ = >(arguments_: T) => arguments_; - { // It should accept input with required property only - const input = {body: {code: ''}}; + { + // It should accept input with required property only + const input = { body: { code: "" } }; function_(input); } - { // It should accept input with optional property - const input = {body: {code: '', name: ''}}; + { + // It should accept input with optional property + const input = { body: { code: "", name: "" } }; function_(input); } - { // It should allow input with excess property - const input = {body: {code: '', name: '', excessProperty: ''}}; + { + // It should allow input with excess property + const input = { body: { code: "", name: "", excessProperty: "" } }; function_(input); } } -{ // Spec - union of array - type Type = Array<{x: string}> & Array<{z: number}>; +{ + // Spec - union of array + type Type = Array<{ x: string }> & Array<{ z: number }>; const function_ = >(arguments_: T) => arguments_; - { // It should accept valid input - const input = [{ - x: '', - z: 1, - }]; + { + // It should accept valid input + const input = [ + { + x: "", + z: 1, + }, + ]; function_(input); } - { // It should reject missing field - const input = [{ - z: 1, - }]; + { + // It should reject missing field + const input = [ + { + z: 1, + }, + ]; // @ts-expect-error function_(input); } - { // It should reject missing field - const input = [{ - x: '', - }]; + { + // It should reject missing field + const input = [ + { + x: "", + }, + ]; // @ts-expect-error function_(input); } - { // It should reject incorrect type - const input = [{ - x: 1, - z: 1, - }]; + { + // It should reject incorrect type + const input = [ + { + x: 1, + z: 1, + }, + ]; // @ts-expect-error function_(input); } - { // It should reject excess field - const input = [{ - x: '', - y: '', - z: 1, - }]; + { + // It should reject excess field + const input = [ + { + x: "", + y: "", + z: 1, + }, + ]; // @ts-expect-error function_(input); } } -{ // Spec - union of readonly array + non readonly array - type Type = ReadonlyArray<{x: string}> & Array<{z: number}>; +{ + // Spec - union of readonly array + non readonly array + type Type = ReadonlyArray<{ x: string }> & Array<{ z: number }>; const function_ = >(arguments_: T) => arguments_; - { // It should accept valid input - const input = [{ - x: '', - z: 1, - }]; + { + // It should accept valid input + const input = [ + { + x: "", + z: 1, + }, + ]; function_(input); } - { // It should reject missing field - const input = [{ - z: 1, - }]; + { + // It should reject missing field + const input = [ + { + z: 1, + }, + ]; // @ts-expect-error function_(input); } - { // It should reject missing field - const input = [{ - x: '', - }]; + { + // It should reject missing field + const input = [ + { + x: "", + }, + ]; // @ts-expect-error function_(input); } - { // It should reject incorrect type - const input = [{ - x: 1, - z: 1, - }]; + { + // It should reject incorrect type + const input = [ + { + x: 1, + z: 1, + }, + ]; // @ts-expect-error function_(input); } - { // It should reject excess field - const input = [{ - x: '', - y: '', - z: 1, - }]; + { + // It should reject excess field + const input = [ + { + x: "", + y: "", + z: 1, + }, + ]; // @ts-expect-error function_(input); } } -{ // Spec - union of array with nested fields - type Type = Array<{x: string}> & Array<{z: number; d: {e: string; f: boolean}}>; +{ + // Spec - union of array with nested fields + type Type = Array<{ x: string }> & + Array<{ z: number; d: { e: string; f: boolean } }>; const function_ = >(arguments_: T) => arguments_; - { // It should accept valid input - const input = [{ - x: '', - z: 1, - d: { - e: 'test', - f: true, + { + // It should accept valid input + const input = [ + { + x: "", + z: 1, + d: { + e: "test", + f: true, + }, }, - }]; + ]; function_(input); } - { // It should reject excess field - const input = [{ - x: '', - z: 1, - d: { - e: 'test', - f: true, - g: '', // Excess field + { + // It should reject excess field + const input = [ + { + x: "", + z: 1, + d: { + e: "test", + f: true, + g: "", // Excess field + }, }, - }]; + ]; // @ts-expect-error function_(input); } - { // It should reject missing field - const input = [{ - x: '', - z: 1, - d: { - e: 'test', - // Missing f: boolean + { + // It should reject missing field + const input = [ + { + x: "", + z: 1, + d: { + e: "test", + // Missing f: boolean + }, }, - }]; + ]; // @ts-expect-error function_(input); } - { // It should reject missing field - const input = [{ - x: '', - z: 1, - d: { - e: 'test', - f: '', // Type mismatch + { + // It should reject missing field + const input = [ + { + x: "", + z: 1, + d: { + e: "test", + f: "", // Type mismatch + }, }, - }]; + ]; // @ts-expect-error function_(input); } } -// Spec - special test case for Opaque types +// Spec - special test case for Tagged types // @see https://github.com/sindresorhus/type-fest/issues/508 { - type SpecialName = Opaque; + type SpecialName = Tagged; type OnlyAcceptName = { name: SpecialName; }; - const onlyAcceptNameImproved = >(arguments_: T) => arguments_; + const onlyAcceptNameImproved = >( + arguments_: T + ) => arguments_; onlyAcceptNameImproved({ // The error before the workaround: // Error: Type 'SpecialName' is not assignable to type 'never' - name: 'name' as SpecialName, + name: "name" as SpecialName, }); } -// Spec - special test case for Opaque type +// Spec - special test case for Tagged type // @see https://github.com/sindresorhus/type-fest/issues/508 { - // Test for number Opaque type - type SpecialName = Opaque; + // Test for number Tagged type + type SpecialName = Tagged; type OnlyAcceptName = { name: SpecialName; }; - const function_ = >(arguments_: T) => arguments_; + const function_ = >(arguments_: T) => + arguments_; function_({ // The error before the workaround: @@ -392,13 +475,14 @@ import type {Exact, Opaque} from '../index'; // Spec - test the above for tagged types too. { - type TaggedNumber = Opaque; + type TaggedNumber = Tagged; - const function_ = >(arguments_: T) => arguments_; + const function_ = >(arguments_: T) => + arguments_; - function_({a: 1 as TaggedNumber}); + function_({ a: 1 as TaggedNumber }); // @ts-expect-error - function_({a: 1 as TaggedNumber, b: true}); + function_({ a: 1 as TaggedNumber, b: true }); } // Spec - special test case for deep optional union @@ -407,25 +491,27 @@ import type {Exact, Opaque} from '../index'; type ParameterType = { outer?: { inner?: { - union: 'foo' | 'bar'; + union: "foo" | "bar"; }; }; }; - const function_ = >(arguments_: InputT) => arguments_; + const function_ = >( + arguments_: InputT + ) => arguments_; // Test input with declared type type Input = { outer?: { inner?: { - union: 'foo' | 'bar'; + union: "foo" | "bar"; }; }; }; const variableWithDeclaredType: Input = { outer: { inner: { - union: 'foo', + union: "foo", }, }, }; @@ -435,7 +521,7 @@ import type {Exact, Opaque} from '../index'; const variableWithoutDeclaredType = { outer: { inner: { - union: 'foo' as const, + union: "foo" as const, }, }, }; @@ -445,7 +531,7 @@ import type {Exact, Opaque} from '../index'; function_({ outer: { inner: { - union: 'foo', + union: "foo", }, }, }); diff --git a/test-d/readonly-deep.ts b/test-d/readonly-deep.ts index c9d284a84..e40037577 100644 --- a/test-d/readonly-deep.ts +++ b/test-d/readonly-deep.ts @@ -1,5 +1,5 @@ import {expectType, expectAssignable} from 'tsd'; -import type {Opaque, tag} from '../source/opaque'; +import type {Tagged, tag} from '../source/tagged'; import type {ReadonlyDeep, ReadonlyObjectDeep} from '../source/readonly-deep'; import type {JsonValue} from '../source/basic'; @@ -17,8 +17,8 @@ type NamespaceWithOverload = Overloaded & { baz: boolean[]; }; -type OpaqueObjectData = {a: number[]} | {b: string}; -type OpaqueObject = Opaque; +type TaggedObjectData = {a: number[]} | {b: string}; +type TaggedObject = Tagged; type ReadonlyJsonValue = | {readonly [k: string]: ReadonlyJsonValue} @@ -62,7 +62,7 @@ const data = { readonlyArray: ['foo'] as readonly string[], readonlyTuple: ['foo'] as const, json: [{x: true}] as JsonValue, - opaqueObj: {a: [3]} as OpaqueObject, // eslint-disable-line @typescript-eslint/consistent-type-assertions + opaqueObj: {a: [3]} as TaggedObject, // eslint-disable-line @typescript-eslint/consistent-type-assertions }; const readonlyData: ReadonlyDeep = data; @@ -99,7 +99,7 @@ expectType>>(readonlyData.readonlySet); expectType(readonlyData.readonlyArray); expectType(readonlyData.readonlyTuple); expectAssignable(readonlyData.json); -expectAssignable, ReadonlyDeep>>(readonlyData.opaqueObj); +expectAssignable & ReadonlyDeep<{[tag]: TaggedObject[typeof tag]}>>(readonlyData.opaqueObj); expectType<((foo: number) => string) & ReadonlyObjectDeep>(readonlyData.namespace); expectType(readonlyData.namespace(1)); diff --git a/test-d/opaque.ts b/test-d/tagged.ts similarity index 60% rename from test-d/opaque.ts rename to test-d/tagged.ts index cb5b1f204..33b24db68 100644 --- a/test-d/opaque.ts +++ b/test-d/tagged.ts @@ -1,37 +1,34 @@ import {expectAssignable, expectNotAssignable, expectNotType, expectType} from 'tsd'; -import type {Opaque, UnwrapOpaque, Tagged, GetTagMetadata, UnwrapTagged, InvariantOf, SnakeCasedPropertiesDeep} from '../index'; +import type {Tagged, GetTagMetadata, UnwrapTagged, InvariantOf, SnakeCasedPropertiesDeep} from '../index'; -type Value = Opaque; +type TaggedValue = Tagged; // We make an explicit cast so we can test the value. -const value: Value = 2 as Value; +const value = 2 as TaggedValue; // The underlying type of the value is still a number. expectAssignable(value); -// You cannot modify an opaque value (and still get back an opaque value). -expectNotAssignable(value + 2); +// If you modify a tagged value, the result is not still tagged. +expectNotAssignable(value + 2); // But you can modify one if you're just treating it as its underlying type. expectAssignable(value + 2); -type WithoutToken = Opaque; -expectAssignable(2 as WithoutToken); - -// Verify that the Opaque's token can be the parent type itself. +// Verify that a tag's medatadata can be the parent type itself. type Person = { - id: Opaque; + id: Tagged; name: string; }; const person = { - id: 42 as Opaque, + id: 42 as Tagged, name: 'Arthur', }; expectType(person); // Failing test for https://github.com/sindresorhus/type-fest/issues/108 -// Use `Opaque` value as `Record` index type. -type UUID = Opaque; +// Use `Tagged` value as `Record` index type. +type UUID = Tagged; type NormalizedDictionary = Record; type Foo = {bar: string}; @@ -45,41 +42,6 @@ const johnsId = '7dd4a16e-d5ee-454c-b1d0-71e23d9fa70b' as UUID; const userJohn = userEntities[johnsId]; 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); - -// UnwrapOpque should work even when the token _happens_ to make the Opaque type -// have the same underlying structure as a Tagged type. -expectType(4 as UnwrapOpaque>); - -// All the basic tests that apply to Opaque types should pass for Tagged types too. -// See rationale for each test in the Opaque tests above. -// -// Tests around not providing a token, which Tagged requires, or using non- -// `string | number | symbol` tags, which Tagged doesn't support, are excluded. -type TaggedValue = Tagged; -type TaggedUUID = Tagged; - -const taggedValue: TaggedValue = 2 as TaggedValue; -expectAssignable(taggedValue); -expectNotAssignable(value + 2); -expectAssignable(value + 2); - -const userEntities2: Record = { - ['7dd4a16e-d5ee-454c-b1d0-71e23d9fa70b' as UUID]: {bar: 'John'}, - ['6ce31270-31eb-4a72-a9bf-43192d4ab436' as UUID]: {bar: 'Doe'}, -}; - -const johnsId2 = '7dd4a16e-d5ee-454c-b1d0-71e23d9fa70b' as TaggedUUID; - -const userJohn2 = userEntities2[johnsId2]; -expectType(userJohn2); - // Tagged types should support multiple tags, // by intersection or repeated application of Tagged. type AbsolutePath = Tagged; @@ -97,27 +59,27 @@ expectAssignable('' as NormalizedAbsolutePath); expectNotAssignable('' as UrlString); expectAssignable('' as SpecialCacheKey); -// A tag that is a union type should be treated as multiple tags. -// This is the only practical-to-implement behavior, given how we're storing the tags. -// However, it's also arguably the desirable behavior, and it's what the TS team planned to implement: -// https://github.com/microsoft/TypeScript/pull/33290#issuecomment-529710519 -expectAssignable>(4 as Tagged); - -// UnwrapOpaque and UnwrapTagged both work on Tagged types. -type PlainValueUnwrapOpaque = UnwrapOpaque; -type PlainValueUnwrapTagged = UnwrapTagged; - -const unwrapped1 = 123 as PlainValueUnwrapOpaque; -const unwrapped2 = 123 as PlainValueUnwrapTagged; +// Remove tag from tagged value. UnwrapTagged should work on types with multiple tags. +type PlainValue = UnwrapTagged; +expectAssignable(123); +const unwrapped1 = 123 as UnwrapTagged; +const unwrapped2 = '' as UnwrapTagged; expectType(unwrapped1); -expectType(unwrapped2); +expectType(unwrapped2); -// UnwrapTagged/UnwrapOpaque should work on types with multiple tags. +const plainValue: PlainValue = 123 as PlainValue; +expectNotType(plainValue); + +// UnwrapTagged should work on types with multiple tags. const unwrapped3 = '' as UnwrapTagged; -const unwrapped4 = '' as UnwrapOpaque; expectType(unwrapped3); -expectType(unwrapped4); + +// A tag that is a union type should be treated as multiple tags. +// This is the only practical-to-implement behavior, given how we're storing the tags. +// However, it's also arguably the desirable behavior, and it's what the TS team planned to implement: +// https://github.com/microsoft/TypeScript/pull/33290#issuecomment-529710519 +expectAssignable>(4 as Tagged); // Tags have no metadata by default expectType(undefined as unknown as GetTagMetadata); @@ -143,6 +105,6 @@ expectAssignable>>( ); // Test for issue https://github.com/sindresorhus/type-fest/issues/643 -type IdType = Opaque; +type IdType = Tagged; type TestSnakeObject = SnakeCasedPropertiesDeep<{testId: IdType}>; expectType({test_id: 2 as IdType}); diff --git a/test-d/writable-deep.ts b/test-d/writable-deep.ts index a97e7ade0..d1c744a58 100644 --- a/test-d/writable-deep.ts +++ b/test-d/writable-deep.ts @@ -1,7 +1,7 @@ import {expectType, expectAssignable} from 'tsd'; -import type {JsonValue, Opaque, ReadonlyDeep, WritableDeep} from '../index'; +import type {JsonValue, Tagged, ReadonlyDeep, WritableDeep} from '../index'; import type {WritableObjectDeep} from '../source/writable-deep'; -import {type tag} from '../source/opaque'; +import {type tag} from '../source/tagged'; type Overloaded = { (foo: number): string; @@ -17,8 +17,8 @@ type NamespaceWithOverload = Overloaded & { readonly baz: readonly boolean[]; }; -type OpaqueObjectData = {readonly a: number[]} | {readonly b: string}; -type OpaqueObject = Opaque; +type TaggedObjectData = {readonly a: number[]} | {readonly b: string}; +type TaggedObject = Tagged; type ReadonlyJsonValue = | {readonly [k: string]: ReadonlyJsonValue} @@ -58,7 +58,7 @@ const data = { readonlyArray: ['foo'] as readonly string[], readonlyTuple: ['foo'] as const, json: [{x: true}] as JsonValue, - opaqueObj: {a: [3]} as OpaqueObject, // eslint-disable-line @typescript-eslint/consistent-type-assertions + opaqueObj: {a: [3]} as TaggedObject, // eslint-disable-line @typescript-eslint/consistent-type-assertions }; const readonlyData: ReadonlyDeep = data; @@ -97,7 +97,7 @@ expectType>(writableData.readonlySet); expectType(writableData.readonlyArray); expectType<['foo']>(writableData.readonlyTuple); expectAssignable(writableData.json); -expectAssignable, WritableDeep>>(writableData.opaqueObj); +expectAssignable & WritableDeep<{[tag]: TaggedObject[typeof tag]}>>(writableData.opaqueObj); expectType<((foo: number) => string) & WritableObjectDeep>(writableData.namespace); expectType(writableData.namespace(1));