A Typescript library for creating discriminating union types. Requires Typescript 3.5 or higher.
Typescript Handbook on discriminating union types
import { impl, matchExhaustive, Variant } from "@practical-fp/union-types"
type Shape =
| Variant<"Circle", { radius: number }>
| Variant<"Square", { sideLength: number }>
const { Circle, Square } = impl<Shape>()
function getArea(shape: Shape) {
return matchExhaustive(shape, {
Circle: ({ radius }) => Math.PI * radius ** 2,
Square: ({ sideLength }) => sideLength ** 2,
})
}
const circle = Circle({ radius: 5 })
const area = getArea(circle)
$ npm install @practical-fp/union-types
import { Variant } from "@practical-fp/union-types"
type Shape =
| Variant<"Circle", { radius: number }>
| Variant<"Square", { sideLength: number }>
This is equivalent to the following type:
type Shape =
| { tag: "Circle", value: { radius: number } }
| { tag: "Square", value: { sideLength: number } }
import { impl } from "@practical-fp/union-types"
const { Circle, Square } = impl<Shape>()
impl<>()
can only be used if your environment has full support
for Proxies. Alternatively, use the constructor<>()
function.
import { constructor } from "@practical-fp/union-types"
const Circle = constructor<Shape, "Circle">("Circle")
const Square = constructor<Shape, "Square">("Square")
Circle
and Square
can then be used to wrap values as a Shape
.
const circle: Shape = Circle({ radius: 5 })
const square: Shape = Square({ sideLength: 3 })
Circle.is
and Square.is
can be used to check if a shape is a circle or a square.
They also act as a type guard.
const shapes: Shape[] = [circle, square]
const sideLengths = shapes.filter(Square.is).map(square => square.value.sideLength)
You can also create custom implementations using the tag()
and predicate()
helper functions.
import { predicate, tag } from "@practical-fp/union-types"
const Circle = (radius: number) => tag("Circle", { radius })
const isCircle = predicate("Circle")
const Square = (sideLength: number) => tag("Square", { sideLength })
const isSquare = predicate("Square")
import { matchExhaustive } from "@practical-fp/union-types"
function getArea(shape: Shape) {
return matchExhaustive(shape, {
Circle: ({ radius }) => Math.PI * radius ** 2,
Square: ({ sideLength }) => sideLength ** 2,
})
}
matchExhaustive()
is exhaustive, i.e., you need to match against every variant of the union.
Cases can be omitted when using a wildcard case with matchWildcard()
.
import { matchWildcard, WILDCARD } from "@practical-fp/union-types"
function getDiameter(shape: Shape) {
return matchWildcard(shape, {
Circle: ({ radius }) => radius * 2,
[WILDCARD]: () => undefined,
})
}
switch
-statements can also be used to match against a union.
import { assertNever } from "@practical-fp/union-types"
function getArea(shape: Shape) {
switch (shape.tag) {
case "Circle":
return Math.PI * shape.value.radius ** 2
case "Square":
return shape.value.sideLength ** 2
default:
// exhaustiveness check
// compile-time error if a case is missing
assertNever(shape)
}
}
impl<>()
and constructor<>()
also support generic union types.
In case the variant type uses unconstrained generics,
unknown
needs to be passed as its type arguments.
import { impl, Variant } from "@practical-fp/union-types"
type Result<T, E> =
| Variant<"Ok", T>
| Variant<"Err", E>
const { Ok, Err } = impl<Result<unknown, unknown>>()
In case the variant type uses constrained generics, the constraint type needs to be passed as its type arguments.
import { impl, Variant } from "@practical-fp/union-types"
type Result<T extends object, E> =
| Variant<"Ok", T>
| Variant<"Err", E>
const { Ok, Err } = impl<Result<object, unknown>>()
strictImpl<>()
and strictConstructor<>()
impl<>()
and constructor<>()
generate generic constructor functions.
This may not always be desirable.
import { impl } from "@practical-fp/union-types"
const { Circle } = impl<Shape>()
const circle = Circle({
radius: 5,
color: "red",
})
Since Circle
is generic, it's perfectly fine to pass extra properties other than radius
.
To prevent that, we can use strictImpl<>()
or strictConstructor<>()
to create a strict
implementation which is not generic.
import { strictImpl } from "@practical-fp/union-types"
const { Circle } = strictImpl<Shape>()
const circle = Circle({
radius: 5,
color: "red", // compile error
})