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> // falsetype R = IsNever<any, { $any: 'any' }> // 'any'
type R = IsNever<unknown> // falsetype R = IsNever<unknown, { $unknown: 'unknown' }> // 'unknown'
type R = IsNever<never> // truetype R = IsNever<never, { $then: 'never' }> // 'never'
type R = IsNever<123> // falsetype 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' : falsetype 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.
Branch Selectors
Section titled “Branch Selectors”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 ? ...: neverThe $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
Section titled “$ResolveBranch”$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 partstype 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.
Branch options
Section titled “Branch options”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.
Special types
Section titled “Special types”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> // truetype A = $Special<unknown> // truetype A = $Special<never> // truetype A = $Special<void> // truetype A = $Special<number> // falseIt can also act as a filter:
type A = $Special<any, { selection: 'filter' }> // anytype A = $Special<number, { selection: 'filter' }> // neverMost 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: ...,}>Type-level errors
Section titled “Type-level errors”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>$Type and the $ prefix
Section titled “$Type and the $ prefix”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.