Skip to content

type branching

Introduced in v8, type branching is a way to finely control output of the type utility.

Using IsNever as an example, it supports 4 branches:

type R = IsNever<Input, {
$any: ...,
$unknown: ...,
$then: ...,
$else: ...,
}>

These branches allow you to control the behavior on those specific cases:

type R = IsNever<any> // false
type R = IsNever<any, { $any: 'any' }> // 'any'
type R = IsNever<unknown> // false
type R = IsNever<unknown, { $unknown: 'unknown' }> // 'unknown'
type R = IsNever<never> // true
type R = IsNever<never, { $then: 'never' }> // 'never'
type R = IsNever<123> // false
type R = IsNever<123, { $else: 'else' }> // 'else'

Without this, you would have to write conditional type to achieve the same result:

type R = IsAny<any> extends true ? 'any' : IsNever<any>
type R = IsUnknown<unknown> extends true ? 'unknown' : IsNever<unknown>
type R = IsNever<never> extends true ? 'never' : false
type R = IsNever<123> extends true ? true : 'else'

It could also get more complicated with the special types (any, unknown, never, void), as well as union and intersection types.

Besides customizing the output, type branching is also useful for type level programming.

When you are using some type utils within your type, you may want to do different things based on the type of the input.

In this case, only using the construct above may not be enough, because the branching can be influenced by the type of the input.

Instead, you can use the branch selectors:

type YourType<T> = IsNever<T, IsNever.$Branch> extends infer B
? B extends $Any ? ...
: B extends $Unknown ? ...
: B extends $Then ? ...
: B extends $Else ? ...
: never

The $Options in IsNever<T, $Options> defines the options, branches, and their default values. The IsNever.$Branch replaces the branches with the branch selectors.

Each branch selector is a named interface that extends $Branch, and carries its branch name in the ~type-plus/branch key:

interface $Then extends $Branch<'$then'> {}
type R = $Then['~type-plus/branch'] // '$then'
type R = $Then[$Branch.$Key] // '$then'

$Branch.$Key is that key. Prefer it in type-level code over spelling the key out, so your code follows the key if it is renamed.

A selector is not a string, so it cannot be mistaken for a real result (IsString<$Then> is false), and selectors with different names are distinct. Errors and hovers print it by name, as $Then.

Do not treat a selector as a string: $Then extends string is false. To read its name, use $B[$Branch.$Key], or match the selector with extends.

You can still provide options using intersection:

type YourType<T> = IsNever<T, IsNever.$Branch & { selection: 'filter' }> extends infer B
...

$ResolveBranch is a utility to resolve the branch selector. You may notice that in the example above, there could be a overlap between the branches:

type R = IsNever<any, { $any: 'any', $else: 'else' }> // 'any' or `else`?

Since properties in object type are not ordered, i.e. { $any: 'any', $else: 'else' } is the same as { $else: 'else', $any: 'any' }, there needs to be a way to define and resolve the branch in a particular order.

$ResolveBranch is the type used within IsNever to handle this:

// only emphasize the relevant parts
type IsNever<T, $O> = $Special<T, {
$any: $ResolveBranch<..., [$Any, $Else]>,
...
}>

The $ResolveBranch will check the result and return the first matching branch selector in the array.

You can use it to develop your own type utils with type branching support.

To take part in branching, a type accepts a $O options parameter carrying one property per branch. A branch property is named $<key> and is typically typed as unknown; its key doubles as the branch identifier.

type IsAny<T, $O extends {
$unknown?: unknown
$never?: unknown
$then?: unknown
$else?: unknown
}> = ...

The real definition of IsAny looks different, but the shape is the same. Those four keys are not declared by hand — each comes from the option type that owns it: $Unknown.$Options, $Never.$Options and $Selection.Options. When you write a type of your own with branching support, compose the option types for the branches you want rather than restating their keys.

Declare the composed options as an interface rather than a type alias. An interface keeps its name, so an error on a bad option names YourType.$Options instead of expanding every part, and it costs the compiler slightly less work per use:

namespace YourType {
export interface $Options
extends $Selection.Options,
$InputOptions<$Any | $Unknown> {}
}

Constrain $O with $StrictOptions so a misspelled key is an error, as it is for the type-plus types:

type YourType<T, $O extends $StrictOptions<$O, YourType.$Options> = {}> = ...

A generic $O has to be forwarded to another type with $ForwardOptions. See Unknown option keys and generic wrappers.

$Selection also holds ready-made option objects that pick a branch style: $Selection.Predicate returns true or false, which is what a type does when $O is {}; $Selection.Filter<T> returns T or never; and $Selection.Branch returns $Then or $Else. $Selection.$BaseOptions is the $then/$else half of $Selection.Options, for a type that has no selection option, such as And.

Two more help you build the inverse of an existing type. $Selection.Flip<$O> swaps the $then and $else options, and $Selection.Invert<B> swaps a $Then selector for $Else and back:

type IsNotBoolean<T> = IsBoolean<T, $Selection.Flip<$Selection.Predicate>>
type R = $Selection.Invert<$Then> // $Else

Selection branching is the most common kind, and $Selection is named after “selection” in the three building blocks of structured programming: sequence, selection, and iteration.

$BranchOptions<$B> builds the options object that maps each branch to its selector, which is how a type’s $Branch member is written:

type R = $BranchOptions<$Then | $Else> // { $then: $Then, $else: $Else }

$Selection.Options is one of several option types a predicate composes. Each covers one option and comes with a Default:

  • $Distributive holds distributive, which defaults to true. With { distributive: false } a predicate judges a union as a whole instead of each member, so IsObject<{} | 1, { distributive: false }> is false rather than boolean.
  • $Exact holds exact, which defaults to false. With { exact: true } a predicate asks whether T is exactly the type rather than assignable to it, so IsObject<{}, { exact: true }> is false.
  • $Fail holds $fail, the value a transform returns when it cannot compute a result, such as Add<number, 1>. $Fail.$Default sets it to never.

$Distributive.Parse<$O> and $Exact.Parse<$O> resolve the caller’s option against the default, so your own type does not repeat the default value:

type R = $Distributive.Parse<{}> // true
type R = $Exact.Parse<{ exact: true }, { $then: 'yes', $else: 'no' }> // 'yes'

Two utilities combine option values. $ResolveOptions<[A, B, ...]> returns the first value that is neither unknown nor undefined, which is how an option that was left out falls back to its default. $MergeOptions<$O, $P> merges $P over $O, so the keys in $P win:

type R = $ResolveOptions<[undefined, false]> // false
type R = $MergeOptions<{ $any: 1, $else: 2 }, { $any: 2 }> // { $else: 2 } & { $any: 2 }

any, unknown, never and void are TypeScript’s special types — the top and bottom of the type system, with no corresponding JavaScript value. Type-level code usually has to treat them apart from everything else, and $Special is the utility for that.

By default it is a predicate:

type A = $Special<any> // true
type A = $Special<unknown> // true
type A = $Special<never> // true
type A = $Special<void> // true
type A = $Special<number> // false

It can also act as a filter:

type A = $Special<any, { selection: 'filter' }> // any
type A = $Special<number, { selection: 'filter' }> // never

Most of the time it is used with branching, to give each special type its own answer:

type A<T> = $Special<T, {
$any: ...,
$unknown: ...,
$never: ...,
$void: ...,
$then: ...,
$else: ...,
}>

Each special type has a branch selector: $Any, $Unknown, $Never and $Void. Each also has an $Options member that declares its branch key, and a $Branch member that sets the branch to the selector:

type R = IsAny<void, { $void: 'void' }> // 'void'
type R = IsAny<void, $Void.$Branch> // $Void

$NotNever is the selector IsNever returns from its filter when T is not never. A filter usually signals a failed check with never, which is the value IsNever is checking for:

type R = IsNever<1, { selection: 'filter' }> // $NotNever

A type-level computation cannot throw, so it has to return something that says it failed. $Error<M, V = unknown> is that something — the type-level analogue of the Error class. M is the message and V optionally carries the value that caused it.

type YourComplexType<A, B, C> = A extends B
? ... // and many more hard work later...
: $Error<'you are doing something wrong!', C>

$InferError<M, T> covers the narrower case of an inference that did not go the way you expected. T extends infer U extends V lets you constrain an inferred type, at the cost of an extra conditional; $InferError is what you return from its else branch.

type F<T> = T extends infer U extends V
? ...your type logic...
: $InferError<'some message', T>

$ErrorMessage<M> is a string type that carries an error message for a constraint to report. $StrictOptions uses it to name a misspelled option key. See Unknown option keys and generic wrappers.

$Fn is a type function, and $Fn.Apply<F, A> calls it. A predicate’s .$Fn, such as IsObject.$Fn<{ exact: true }>, is the predicate with its options applied. You can pass one to Filter, Find, Some or DropMatch. See passing predicates to collection types.

Types named with a $ prefix are building blocks for writing types, not types to reach for in application code.

$Type underpins them. It is a branded type used to define types that are unique at the type level, and it supports both primitives and object types. For an object type it intersects with the type you give it, so the resulting type still exposes that type’s properties directly. It stores the type and value in the internal properties _$type and _$value, so the type you pass in should not declare properties by those names. Pass $O: { bare: true } if you want the brand without the intersection.

The branch selectors do not use $Type; they extend $Branch, described in branch selectors.