Skip to content

Migrating from Then/Else to $Options

Before v8, a branching type took its two outcomes as positional type parameters:

type IsEqual<A, B, Then = true, Else = false> = ...

That shape ran out of room. There was nowhere to put behavioral options such as 🔀 distributive and 📌 exact, and nowhere to name the outcome for a special input such as any or never.

v8 replaces it with a single options object, $O:

type IsEqual<A, B, $O extends IsEqual.$Options = {}> = ...

Then becomes $then and Else becomes $else.

// before
type R = IsString<T, 'yes', 'no'>
// after
type R = IsString<T, { $then: 'yes'; $else: 'no' }>

Everything else the old form could express, it expressed by picking different Then and Else values. Those have shorthands now:

Before After
IsString<T> IsString<T>
IsString<T, T, never> IsString<T, { selection: 'filter' }>
IsString<T, Then, Else> IsString<T, { $then: Then; $else: Else }>

And the options object holds the things the positional form had no room for:

type R1 = IsString<'a', { exact: true }> // false
type R2 = IsString<'a' | 1, { distributive: false }> // false
type R3 = IsNever<any, { $any: 'any'; $then: 'never'; $else: 'no' }> // 'any'

See Options for the full list and type branching for the branch selectors.

The IsXXX predicate family — IsAny, IsArray, IsBigint, IsBoolean, IsFalse, IsFunction, IsNever, IsNull, IsNumber, IsObject, IsStrictFunction, IsString, IsSymbol, IsTrue, IsTuple, IsUndefined, IsUnknown, IsVoid — and Assignable take $O and no longer accept the positional form.

The last of the positional branching types moved in 8.0.0 too:

Before After
If<Condition, Then, Else> If<Condition, { $then: Then; $else: Else }>
HasKey<T, K, Then, Else> HasKey<T, K, { $then: Then; $else: Else }>
IsOptionalKey<T, K, Then, Else> IsOptionalKey<T, K, { $then: Then; $else: Else }>
IsUnion<T, Then, Else> IsUnion<T, { $then: Then; $else: Else }>
UnionType<T> IsUnion<T, { selection: 'filter' }>
IsIndexOutOfBound<A, N, Then, Else> IsIndexOutOfBound<A, N, { $then: Then; $else: Else }>
StringPlus.Includes<S, Search, Then, Else> StringPlus.Includes<S, Search, { $then: Then; $else: Else }>

UnionType was the one entry that collapsed rather than moved: it defaulted to Then = T, Else = never, which is filter semantics, and $O spells that natively — so the UnionType / IsUnion pair is now one type plus an option.

// before
type R = UnionType<T> // T if T is a union, never otherwise
// after
type R = IsUnion<T, { selection: 'filter' }>

HasKey and IsOptionalKey take the key as the filter subject, so their filter form selects keys: IsOptionalKey<T, keyof T, { selection: 'filter' }> is OptionalKeys<T>.

StringIncludes keeps its positional Then/Else. It is the low-level string check; StringPlus.Includes is the branching type built on it.

Positional types that already have an $O replacement

Section titled “Positional types that already have an $O replacement”

These are deprecated aliases. They still work, but the type to reach for already takes $O — you are not waiting on anything, just renaming the call:

Deprecated Use instead
IsEqual, IsNotEqual, NotEqual Equal
CanAssign, StrictCanAssign, IsAssign Assignable
Extendable, NotExtendable, IsExtend, IsNotExtend $Assignable
LooseArrayType, IsLooseArray, NotLooseArrayType, IsNotLooseArray IsArray / IsNotArrayalready removed, see below

Equal is not a drop-in rename in every case — it is stricter than IsEqual about symbol. IsEqual<typeof uniqueSym, symbol> is true; Equal<typeof uniqueSym, symbol> is false. Check symbol comparisons when you migrate.

The LooseArrayType family is the one entry already gone rather than deprecated — it was removed during the 8.0.0 beta, since IsArray is loose by default and no longer needs a stopgap. The per-type mapping lives with the array API, under Loose array types.

If you build types on top of type-plus, the same convention is available to you. Declare the options in a namespace next to the type and resolve the branches with $ResolveBranch:

import type { $ResolveBranch, $Selection, $Then, $Else } from 'type-plus'
export type IsPositive<T extends number, $O extends IsPositive.$Options = {}> =
`${T}` extends `-${string}` ? $ResolveBranch<$O, [$Else]> : $ResolveBranch<$O, [$Then], T>
export namespace IsPositive {
export type $Options = $Selection.Options
export type $Branch<$O extends $Options = {}> = $Selection.Branch<$O>
}

Defaulting $O to {} is what keeps the plain IsPositive<1> call form working: with no branches supplied, $ResolveBranch falls back to the predicate result.