A common pattern in code which needs to uniquely handle different types of data in a (relatively) uniform fashion is to branch and cast for each type to perform the type-specific implementation. This can be slightly cumbersome in some languages, but TypeScript provides a convenient way to combine these steps in a fairly intuitive fashion.
Notably, whenever a branch occurs on data that is of potentially multiple types, as the conditions on the branches winnow the knowledge of what type the data can be, TypeScript will automatically treat the data as belonging to the winnowed types in the appropriate branches. That is, this type narrowing will cast the broader type to shrink to a subset of its definition as TypeScript determines that doing so is safe.
One can imagine, for example, a variable that is typed to allow either numeric or textual data: let data: number|string
. When TypeScript encounters data
, it knows that it can be one of two types, either number
or string
. However, should a branch conditional determine that the contents of the variable is specifically numeric, TypeScript intuits that, within the context of that branch being true, the variable can safely be assumed numeric. Furthermore, this narrows the types for the other branches: in this case, trivially causing the false branch to treat the variable as though it were string
. For more complicated types, the type will be narrowed with each successive failed branch conditional.
TypeScript is not limited to performing type narrowing specifically in the context of an inline type check in a branch conditional. Rather, the idea can be abstracted out to special predicate functions called type guards, which perform boolean operations to ascertain the type information of the data they are given, and then make, based off those operations, an assertion about the resulting type.
A type guard has a special return type: parameter is Type
; that is, the return type mentions the parameter they guard is type asserting, utilizes the is
keyword to signal the type assertion, then provides the type (either inline or predefined) being asserted. TypeScript will then cause true branches downstream of the guard invocation to automatically treat the guard argument to be narrowed.
Guards need only return a boolean value; when transpiled down to JavaScript, they function as standard boolean predicates. These functions do require a bit of trust: one could, for example, write a guard which asserts that everything passed to it is valid, even when this is untrue. As with all programmed systems, having ample test suites which verify the integrity and validity of type guards is very desirable.
The predicate function for a type guard may use whatever criteria it wants to assert truth, just so long as the result is a boolean value. This can include using the typeof
operator to interrogate a variable for its JavaScript prototype type information, using the strict equality operator (===
) to perform a comparison to a known value, checking to see if a field exists within an object's properties via the in
keyword, and so forth.
As type guards are predicates, and predicates are functions, one can even generate new type guards that conform to well defined behavioral checks using higher order functions, generics, and closures. In such a system, the HOF would take a value or conditional function to use in the generated guard, as well as the type to assert in the guard's return type. The generated function can then use this enclosed scope to perform checks on whatever parameter is passed to it.