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, IsString, IsSymbol, IsTrue,
IsTuple, IsUndefined, IsUnknown, IsVoid — and Assignable take $O and no longer accept the
positional form.
IsEmptyObject, IsRecord and IsDisjoint took no options before 8.0.0 and now take $O.
ArrayPlus.IsReadonly moved from the legacy IsReadonly.Options onto $O; its $notArray branch is
gone, so write IsArray<A, { $then: IsReadonly<A>; $else: X }> where you passed { $notArray: X }.
IsStrictFunction and IsNotStrictFunction are removed. Use IsFunction<T, { exact: true }> and
IsNotFunction<T, { exact: true }> instead.
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 }> |
Equal |
IsEqual |
IsEqual<A, B, Then, Else> |
IsEqual<A, B, { $then: Then; $else: Else }> |
NotEqual |
IsNotEqual |
IsNotEqual<A, B, Then, Else> |
IsNotEqual<A, B, { $then: Then; $else: Else }> |
IsLiteral<T, Then, Else> |
IsLiteral<T, { $then: Then; $else: Else }> |
Some<A, Criteria, Mode, Then, Else> |
Some<A, Criteria, { mode: Mode; $then: Then; $else: Else }> |
CanAssign<A, B> |
Assignable<A, B> |
StrictCanAssign<A, B> |
Assignable<A, B, { distributive: false }> |
IsAssign<A, B> |
Assignable<A, B> |
Extendable<A, B> |
Assignable.$<A, B, { selection: 'filter' }> |
NotExtendable<A, B> |
NotAssignable.$<A, B, { selection: 'filter' }> |
IsExtend<A, B, Then, Else> |
Assignable.$<A, B, { $then: Then; $else: Else }> |
IsNotExtend<A, B, Then, Else> |
NotAssignable.$<A, B, { $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>.
Some’s Mode parameter folded into $O as mode rather than moving position for position — it
now sits beside selection and the branch overrides in the one options object:
// beforetype R1 = Some<A, C, 'strict'>type R2 = Some<A, C, 'loose', 'yes', 'no'>
// aftertype R1 = Some<A, C, { mode: 'strict' }>type R2 = Some<A, C, { $then: 'yes'; $else: 'no' }>Equal, IsEqual/IsNotEqual/NotEqual (the old positional forms), CanAssign and its
StrictCanAssign/IsAssign aliases, and Extendable/NotExtendable/IsExtend/IsNotExtend are
gone as of 8.0.0 — there is no deprecation window, the rename above is the only path forward.
IsEqual itself changed behavior along with its shape: two distinct unique symbols are no longer
equal, and a unique symbol is no longer equal to symbol. Before 8.0.0, IsEqual (then a
deprecated alias) reported both of those as equal. Check symbol comparisons when you migrate.
StringIncludes keeps its positional Then/Else by design — it is the low-level string check,
and StringPlus.Includes is the branching type built on top of it.
Positional types already removed outright
Section titled “Positional types already removed outright”The LooseArrayType family — LooseArrayType, IsLooseArray, NotLooseArrayType,
IsNotLooseArray — is removed in 8.0.0, since IsArray is loose by default and no
longer needs a stopgap. Use IsArray / IsNotArray instead. 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, $StrictOptions, $Then, $Else } from 'type-plus'
export type IsPositive<T extends number, $O extends $StrictOptions<$O, IsPositive.$Options> = {}> = `${T}` extends `-${string}` ? $ResolveBranch<$O, [$Else]> : $ResolveBranch<$O, [$Then], T>
export namespace IsPositive { export interface $Options extends $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. $StrictOptions rejects any key
IsPositive.$Options does not declare; see Unknown option keys and generic wrappers
for forwarding a generic $O to another type.