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 = {}> = ...The mapping
Section titled “The mapping”Then becomes $then and Else becomes $else.
// beforetype R = IsString<T, 'yes', 'no'>
// aftertype 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 }> // falsetype R2 = IsString<'a' | 1, { distributive: false }> // falsetype R3 = IsNever<any, { $any: 'any'; $then: 'never'; $else: 'no' }> // 'any'See Options for the full list and type branching for the branch selectors.
What has already moved
Section titled “What has already moved”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.
// beforetype R = UnionType<T> // T if T is a union, never otherwise
// aftertype 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 / IsNotArray — already 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.
Migrating your own types
Section titled “Migrating your own 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.