Union and Mixed Types
A union type combines multiple types with |. A value belongs to a union if it belongs to at least one of its members.
This page covers the union utilities, which detect and constrain unions,
and the mix_types utilities, which work across arbitrary combinations of types rather than a single category.
IsUnion
Section titled “IsUnion”type IsUnion<T, $O extends IsUnion.$Options = {}>🎭 predicate — resolves to true when T is a union, otherwise false.
import type { IsUnion } from 'type-plus'
type R = IsUnion<'a' | 'b'> // truetype R = IsUnion<boolean> // truetype R = IsUnion<number> // falseboolean is a union because it is true | false.
It accepts the full type branching options,
so { selection: 'filter' }, $then/$else, and IsUnion.$Branch all work as usual.
type R = IsUnion<'a' | 'b', { selection: 'filter' }> // 'a' | 'b'type R = IsUnion<number, { selection: 'filter' }> // never
type R = IsUnion<number, { $then: 1; $else: 2 }> // 2IsUnion.$ is the same check exposed as a type util for building custom types.
UnionType
Section titled “UnionType”🗑️ removed in 8.0.0: use IsUnion with { selection: 'filter' }.
| Removed | Replacement |
|---|---|
UnionType<T> |
IsUnion<T, { selection: 'filter' }> |
UnionType<T, Then, Else> |
IsUnion<T, { $then: Then; $else: Else }> |
SubUnion
Section titled “SubUnion”type SubUnion<U, T extends U> = T🧰 type util — declares a type that must be a subset of the union U.
The value of the type is T itself; the constraint is the point.
import type { SubUnion } from 'type-plus'
type Fruit = 'apple' | 'banana' | 'cherry'
type Picked = SubUnion<Fruit, 'apple'> // 'apple'
// @ts-expect-error 'carrot' is not a Fruittype Bad = SubUnion<Fruit, 'carrot'>Use it instead of type Picked = 'apple' when you want the compiler to reject a member that drifts out of the source union.
type Merge<A, B>function merge<A, B>(a: A, b: B): Merge<A, B>⚗️ transform — the type-level equivalent of { ...a, ...b }.
Unlike the object-only merge, A and B are unconstrained: primitives are boxed
(number becomes Number, and so on) before merging, and the special types are handled explicitly.
import { merge, type Merge } from 'type-plus'
type R = Merge<{ a: 1 }, { b: 2 }> // { a: 1; b: 2 }
type R = Merge<{ a: 1 }, never> // nevertype R = Merge<{ a: 1 }, unknown> // { a: 1 }type R = Merge<{ a: 1 }, undefined> // { a: 1 }type R = Merge<{ a: 1 }, void> // { a: 1 } & void
const r = merge({ a: 1 }, {} as { a?: string | undefined }) // { a: number | string }never in either position wins and produces never. unknown, undefined, and null are treated as “nothing to merge” and the other side is returned.
type Box<T, Options extends Box.Options = Box.DefaultOptions>⚗️ transform — converts a primitive type to its boxed object type.
import type { Box } from 'type-plus'
type R = Box<number> // Numbertype R = Box<string> // Stringtype R = Box<'abc'> // Stringtype R = Box<() => void> // Functiontype R = Box<object> // Object
type R = Box<undefined> // nevertype R = Box<undefined, { $notBoxable: 'nope' }> // 'nope'$notBoxable sets what is returned when T has no boxed form. It defaults to never.
Exclude
Section titled “Exclude”type Exclude<T, U, R = never>🌪️ filter — a drop-in replacement for the built-in Exclude<T, U> that can also replace the removed members with R.
import type { Exclude } from 'type-plus'
type R = Exclude<'a' | 'b' | 'c', 'a'> // 'b' | 'c'type R = Exclude<'a' | 'b' | 'c', 'a', 'd'> // 'b' | 'c' | 'd'
type R = Exclude<undefined | 1, undefined, 2> // 1 | 2Importing this shadows the global Exclude in that file, which is intentional — the two-argument form behaves identically.
IsAnyOrNever
Section titled “IsAnyOrNever”type IsAnyOrNever<T, $O extends $Selection.Options = $Selection.Predicate>🎭 predicate 🩳 shortcut — validates that T is exactly any or exactly never.
These two are the cases most type utilities have to special-case first, so this bundles both checks.
import type { IsAnyOrNever } from 'type-plus'
type R = IsAnyOrNever<any> // truetype R = IsAnyOrNever<never> // true
type R = IsAnyOrNever<unknown> // falsetype R = IsAnyOrNever<void> // falsetype R = IsAnyOrNever<1> // falseIt accepts the full type branching options, so { selection: 'filter' },
$then/$else, and the branch selectors all work as usual.