Skip to content

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.

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'> // true
type R = IsUnion<boolean> // true
type R = IsUnion<number> // false
type R = IsNotUnion<number> // true
type R = IsNotUnion<boolean> // false

boolean 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 }> // 2

IsUnion.$ and IsNotUnion.$ are the same checks exposed as a type util for building custom types.

🗑️ 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 }>
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 Fruit
type 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> // never
type 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> // Number
type R = Box<string> // String
type R = Box<'abc'> // String
type R = Box<() => void> // Function
type R = Box<object> // Object
type R = Box<undefined> // never
type R = Box<undefined, { $notBoxable: 'nope' }> // 'nope'

$notBoxable sets what is returned when T has no boxed form. It defaults to never.

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 | 2

Importing 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.

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> // true
type R = IsAnyOrNever<never> // true
type R = IsAnyOrNever<unknown> // false
type R = IsAnyOrNever<void> // false
type R = IsAnyOrNever<1> // false

It accepts the full type branching options, so { selection: 'filter' }, $then/$else, and the branch selectors all work as usual.

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> // false
type R = IsNotAnyOrNever<never> // false
type R = IsNotAnyOrNever<unknown> // true
type R = IsNotAnyOrNever<1> // true

It 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.