From 2e1cec8aaf05351a088dd246eb073c33aff5bb30 Mon Sep 17 00:00:00 2001 From: Pyry Heiskanen Date: Mon, 13 Mar 2023 09:05:24 +0200 Subject: [PATCH] Add `TaggedUnion` type (#566) --- index.d.ts | 1 + readme.md | 1 + source/tagged-union.d.ts | 51 ++++++++++++++++++++++++++++++++++++++++ test-d/tagged-union.ts | 30 +++++++++++++++++++++++ 4 files changed, 83 insertions(+) create mode 100644 source/tagged-union.d.ts create mode 100644 test-d/tagged-union.ts diff --git a/index.d.ts b/index.d.ts index 9bd37ed9..e1cc79d8 100644 --- a/index.d.ts +++ b/index.d.ts @@ -7,6 +7,7 @@ export * from './source/observable-like'; // Utilities export type {EmptyObject, IsEmptyObject} from './source/empty-object'; export type {Except} from './source/except'; +export type {TaggedUnion} from './source/tagged-union'; export type {Writable} from './source/writable'; export type {WritableDeep} from './source/writable-deep'; export type {Merge} from './source/merge'; diff --git a/readme.md b/readme.md index 369583ff..9ddad4b7 100644 --- a/readme.md +++ b/readme.md @@ -178,6 +178,7 @@ Click the type names for complete docs. - [`IsNumericLiteral`](source/is-literal.d.ts) - Returns a boolean for whether the given type is a `number` or `bigint` [literal type](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types). - [`IsBooleanLiteral`](source/is-literal.d.ts) - Returns a boolean for whether the given type is a `true` or `false` [literal type](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types). - [`IsSymbolLiteral`](source/is-literal.d.ts) - Returns a boolean for whether the given type is a `symbol` [literal type](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types). +- [`TaggedUnion`](source/tagged-union.d.ts) - Create a union of types that share a common discriminant property. ### JSON diff --git a/source/tagged-union.d.ts b/source/tagged-union.d.ts new file mode 100644 index 00000000..57ae5305 --- /dev/null +++ b/source/tagged-union.d.ts @@ -0,0 +1,51 @@ +/** +Create a union of types that share a common discriminant property. + +Use-case: A shorter way to declare tagged unions with multiple members. + +@example +``` +import type {TaggedUnion} from 'type-fest'; + +type Tagged = TaggedUnion<'type', Fields> + +// The TaggedUnion utility reduces the amount of boilerplate needed to create a tagged union with multiple members, making the code more concise. +type EventMessage = Tagged<{ + OpenExternalUrl: { + url: string; + id: number; + language: string; + }; + ToggleBackButtonVisibility: { + visible: boolean; + }; + PurchaseButtonPressed: { + price: number; + time: Date; + }; + NavigationStateChanged: { + navigation?: string; + }; +}>; + +// Here is the same type created without this utility. +type EventMessage = + | { + type: 'OpenExternalUrl'; + url: string; + id: number; + language: string; + } + | {type: 'ToggleBackButtonVisibility'; visible: boolean} + | {type: 'PurchaseButtonPressed'; price: number; time: Date} + | {type: 'NavigationStateChanged'; navigation?: string}; +``` + +@category Utilities +*/ +export type TaggedUnion< + TagKey extends string, + UnionMembers extends Record>, +> = { + [Name in keyof UnionMembers]: {[Key in TagKey]: Name} & UnionMembers[Name]; +}[keyof UnionMembers]; diff --git a/test-d/tagged-union.ts b/test-d/tagged-union.ts new file mode 100644 index 00000000..8e30b124 --- /dev/null +++ b/test-d/tagged-union.ts @@ -0,0 +1,30 @@ +import {expectAssignable, expectNotAssignable} from 'tsd'; +import type {TaggedUnion} from '../index'; + +type Union = TaggedUnion<'tag', {str: {a: string} ; num: {b: number}}>; + +const first = { + tag: 'str' as const, + a: 'some-string', +}; + +const second = { + tag: 'num' as const, + b: 1, +}; + +expectAssignable(first); +expectAssignable(second); + +const fails = { + tag: 'num' as const, + b: 'should not be string', +}; + +const failsToo = { + tag: 'str' as const, + b: 2, +}; + +expectNotAssignable(fails); +expectNotAssignable(failsToo);