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.

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> = $SpecialType<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 $SelectionOptions. 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.

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.

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: ...,
}>

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>

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.