A modern TypeScript library designed to reduce boilerplate for tagged unions, also known as discriminated unions.
This library is also an implementation of algebraic data types.
- Effortlessly defines tagged union types, encompassing even recursive ones
- Generates following helper functions for each tagged union type (without code generation 👍)
- Data constructors
- Pattern matching functions
- Type guard functions (type predicates)
- Works on both browsers and Node.js
- 0 dependencies
Here is an example of defining a simple tagged union type and creating its values.
import { type TaggedUnion, createHelperFunctions } from 'ts-tagged-union'
// Define a tagged union type
export type Color = TaggedUnion<{
rgb: { r: number; g: number; b: number }
primary: {}
secondary: {}
}>
// Get helper functions for the type
export const Color = createHelperFunctions<Color>()
// Create object with a data constructor
const rgb = Color.rgb({ r: 255, g: 31, b: 0 })
const primary = Color.primary() // {} can be omitted
console.log(rgb) // { r: 255, g: 31, b: 0, [Symbol(defaultTagKey)]: 'rgb' }
console.log(primary) // { [Symbol(defaultTagKey)]: 'primary' }
To perform pattern matching with exhaustiveness checking, use the match
function.
const color = Math.random() < 0.5 ? Color.primary() : Color.secondary()
const cssColor = Color.match(color, {
rgb: ({ r, g, b }) => `rgb(${r}, ${g}, ${b})`,
primary: () => '#C0FFEE',
secondary: () => 'blue',
})
The third argument serves as a so-called default case, as follows.
const isAchromatic = Color.match(
color,
{ rgb: ({ r, g, b }) => r === g && g === b },
(other) => false,
)
To perform pattern matching without exhaustiveness checking, use the matchPartial
instead.
Type guard functions are available as the is
and isNot
properties, as shown below.
if (Color.is.rgb(color)) {
// Here, the variable is narrowed to the rgb variant type.
console.log(color.r, color.g, color.b)
}
if (Color.isNot.secondary(color)) {
// Here, the variable is narrowed to the rgb or primary variant type.
console.log(color)
}
The key of the property used to distinguish each variant is called tag key.
You can specify a tag key as the second argument to TaggedUnion<T>
as follows.
// Define a tagged union type with a custom tag key, 'status'
type Response = TaggedUnion<
{
Success: { payload: Blob }
Failure: { message: string }
},
'status' // Either a string literal or symbol type
>
// You need to provide the tag key as an argument due to TypeScript specifications.
const Response = createHelperFunctions<Response>('status')
const failure = Response.Failure({ message: 'Not found' })
console.log(failure.status) // Failure
console.log(Response.tagKey) // status
createHelperFunctions
and other utilities do not work for tagged union types without a tag-key-pointer.
The tag-key-pointer is a special hidden property that specifies which property is a tag.
It exists only at the type level, so it does not affect runtime.
The type defined with TaggedUnion<T>
has the tag-key-pointer property.
To manually add it to an existing type, use AddTagKeyPointer
as follows.
import { type AddTagKeyPointer, createHelperFunctions } from 'ts-tagged-union'
type RawTaggedUnion =
| { type: 'circle', radius: number }
| { type: 'rect', width: number; height: number }
type Shape = AddTagKeyPointer<RawTaggedUnion, 'type'>
const Shape = createHelperFunctions<Shape>('type')
If you need to remove the tag-key-pointer, use RemoveTagKeyPointer
.
There are also several other utilities.
Get the tag key of the given tagged union type.
example
type Response = TaggedUnion<
{
Success: { payload: Blob }
Failure: { message: string }
},
'status'
>
type TagKey = TagKeyOf<Response> // 'status'
Extract the variant type with the specific tag from a tagged union type.
example
type Response = TaggedUnion<
{
Success: { payload: Blob }
Failure: { message: string }
},
'status'
>
type Variant = VariantOf<Response, 'Failure'> // { status: 'Failure', message: string }
Extract the payload type of the variant with the specific tag from a tagged union type.
example
type Response = TaggedUnion<
{
Success: { payload: Blob }
Failure: { message: string }
},
'status'
>
type Payload = PayloadOf<Response, 'Failure'> // { message: string }