It is often desireable, in software development, to develop code which exhibits two orthogonal, yet mutually beneficial, properties: type safety and reusability. The former property relates to the set of operations that are valid to be performed on data adhering to specific known types; break the contracts defined by the type, and the system breaks. The latter concern—reusability—relates to the capability to modularize code in such a fashion that it can be used in multiple different contexts with only a modicum of alteration. The ideal is to minimize repetitious boilerplate and maintenance headaches while still ensuring operations performed on data are semantically valid.
Enter generics: a feature of many modern languages which provide the utility of an abstract type for reusability, but which provide mechanisms to enforce type checking. A generic is, itself, a type, but unlike non-generic types, it has a few special properties attached to it.
First, generic types are swapped out with an actual type at the point of invocation. That is, a function written against a generic type will, when called elsewhere in code, replace the generic type with the type information provided at point of call. When TypeScript performs type checking of the invocation, it will ascertain the correctness of the substituted type, rather than the generic. This will—usually—catch type-related semantic errors.
Second, generics provide an abstraction: as they are not tied to any one specific type at the point of function definition and are only realized at point of use, they offer to general use of a type like any
(or, say, object
), while affording type checking later on.
Third, generics may be optionally constrained at the point of definition, limiting the scope of data their associated functions may be applicable to. A generic so constrained would cause TypeScript to flag an error if an incompatible type is used at point of call. Somewhat paradoxically, while constraints limit the applicability of use of a generic function, they open up the set of operations that can be performed on the generic data safely. That is, a constrained generic has more information about its capabilities, allowing code written around it to more fully use the type.
Some potential use cases for generics include using predefined utility types—built-in to TypeScript—to automate and clean some definitions (e.g., using Readonly<>
to make all fields on a type readonly); using a custom generic to define type relationships of other types; and using constrained generics to write code that can be reused in multiple type-safe contexts.
New types—including new generic types—can be defined in terms of other types, including through use of generics. Through careful use of both generic constraints and conditional typing, which will be covered in another article, it is possible to refine the scope of a newly defined type, often to define relationships with other types.
When calling a generic function, TypeScript will attempt to infer the appropriate type contextually; this may sometimes fail outright, or revert to a slightly less appropriate type, depending upon circumstance. It is not always necessary to specify the exact type being bound to the generic type, but doing so can offer a fine-grained degree of control to ensure type correctness. Even so, whether by inference or specification, a substituted type can allow functionality downstream of the generic to correctly allow type safe behavior.
Generics are incredibly useful for writing code which exhibits both flexibility and type correctness. They will be revisited frequently in this series.