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 / IsNotUnion
Section titled “IsUnion / IsNotUnion”type IsUnion<T, $O extends $StrictOptions<$O, IsUnion.$Options> = {}>type IsNotUnion<T, $O extends $StrictOptions<$O, IsNotUnion.$Options> = {}>🎭 predicate — IsUnion resolves to true when T is a union, otherwise false.
IsNotUnion is its inverse, with the same rules.
import type { IsUnion } from 'type-plus'
type R = IsUnion<'a' | 'b'> // truetype R = IsUnion<boolean> // truetype R = IsUnion<number> // false
type R = IsNotUnion<number> // truetype R = IsNotUnion<boolean> // 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.$ and IsNotUnion.$ are the same checks 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, $O extends $StrictOptions<$O, Box.$Options> = {}>⚗️ 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, $O extends $StrictOptions<$O, Exclude.$Options> = {}>🌪️ filter — a drop-in replacement for the built-in Exclude<T, U> that can also replace the removed
members with $O['$excluded'].
import type { Exclude } from 'type-plus'
type R = Exclude<'a' | 'b' | 'c', 'a'> // 'b' | 'c'type R = Exclude<'a' | 'b' | 'c', 'a', { $excluded: 'd' }> // 'b' | 'c' | 'd'
type R = Exclude<undefined | 1, undefined, { $excluded: 2 }> // 1 | 2Importing this shadows the global Exclude in that file. That is intentional: the two-argument form
is identical to the built-in, generic T included, so shadowing it changes nothing.
IsAnyOrNever
Section titled “IsAnyOrNever”type IsAnyOrNever<T, $O extends $StrictOptions<$O, $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.
IsNotAnyOrNever
Section titled “IsNotAnyOrNever”type IsNotAnyOrNever<T, $O extends $StrictOptions<$O, $Selection.Options> = $Selection.Predicate>🎭 predicate — the negation of IsAnyOrNever: validates that T is neither exactly any nor
exactly never.
import type { IsNotAnyOrNever } from 'type-plus'
type R = IsNotAnyOrNever<any> // falsetype R = IsNotAnyOrNever<never> // false
type R = IsNotAnyOrNever<unknown> // truetype R = IsNotAnyOrNever<1> // trueIt accepts the same options as IsAnyOrNever.
There is no HasAny or HasNever to go with HasNull, HasUndefined and HasVoid.
A union absorbs into any and drops never (any | 1 is any, never | 1 is 1),
so “T includes any” is IsAny and “T includes never” is IsNever.