Typescript — Generics and overloads 👨‍🔬


This article is part of the collection “TypeScript Essentials”, this is Chapter four.

In last article “TypeScript — Learn the basics”, we saw all basic usages of TypeScript types.

By the end of this article, 
you’ll understand why generic types are essential for real world usage.

Generic — Configurable types

While basic types like interfaces are useful to describe data and basic functions signatures, generics helps making types “open” and reusable.

Why use Generic types?

Imagine we want to expose the following helper in our application:

function withUID (obj) { 
return Object.assign({}, obj, { uuid: _.uniqueId() });

The more straight-forward way to “type” this function would be:

function withUID (obj: any) { 
return Object.assign({}, obj, { uuid: _.uniqueId() });

We use any because we want to accept any value — indeed we can use object.

The problem is that the inferred return type of the function is any .

By using scalar types (object, …) or any,
we prevent TypeScript to infer the return type.

To overcome this problem, we’re gonna use generics.

Using Generic types

In languages like C# and Java, one of the main tools in the toolbox for creating reusable components is generics, that is, being able to create a component that can work over a variety of types rather than a single one.


As explained this excerpt of the TypeScript documentation, we want to create functions that accept many kind of types, without losing type — without using any.

Let’s see the withUID using generics:

function withUID<T>(obj: T) { 
return Object.assign({}, obj, { uuid: _.uniqueId() });

The <> syntax is reserved for describing generic type.
A generic expose many “type arguments”, listed between the <>.

Generics can be applied to interfaces , class and function.

interface A<T, S> {
a: T;
b: S;
c: { id: string } & S;

As you can see, we can define as many “type argument” as needed.

Here, T type is inferred from the passed argument type.

If no type argument type is explicitly passed, TypeScript will try to infer them by the values passed to the function arguments.

Example, for withUID, T is inferred from the type of obj argument.

Generics can “extends”

Type argument can provide some constraints by using the extends keyword.

function withUID<T extends object>(obj: T) { 
return Object.assign({}, obj, { uuid: _.uniqueId() });
withUID({ a: 1 }); // is valid
withUID("hello"); // is NOT valid

Here, T should fulfil the condition “is an object type”.

interface Person { name: string; }
function withUID<T extends Person>(obj: T) { 
return Object.assign({}, obj, { uuid: _.uniqueId() });
withUID({ name: "POLY", surname: "Chack" }); // is valid

Generics can have a “default type value”

Finally, argument types can also have “default” or “computed value”.

interface A<T=string> {
name: T;
const a:A = { name: "Charly" };
const a:A<number> = { name: 101 };

This is particularly important for interfaces which unlike function generics cannot omit type argument, example:

For interfaces, TypeScript cannot infer type arguments based on properties value, unlike for functions

That’s why “default type value” is a “nice to know”:

This, is correct.

Tips — default type arguments can reuse other type arguments

The example below describe how we can define a type argument on-top of another:

function MyFunction<T extends Person, S=T&{ ssid: string }>(
person: S
): S {
/* ... */

Overloads — Extendable Function types

Generics are convenient for function that are responsible of simple isolated computation or predictable return given a input.

Unfortunately, everything isn’t this simple, some “big” functions may have high complexity with variant return type.
Generics can be use in combination with “overloads” to overcome this issue.

Example, how can we type this function ?

function getArray(...args) {
if (args.length === 1 && typeof args[0] === 'number') {
return new Array(args[0])
} else if (args.length > 1) {
return Array.from(args);
getArray(5) // => [undefined x 5]
getArray('a', 'b', 'c') // => ['a', 'b', 'c']

Here is the inferred result type:

The solution is to use “Overloads”.

The answer is to supply multiple function types for the same function as a list of overloads. This list is what the compiler will use to resolve function calls.


Overloads is the fact of listing all the possible input/output types couple for a given function.


ℹ️ Please note that :

  • overloads have no body
  • the implementation function must have as open as possible typing 
    (to allow overloading)

Real world examples

interface with Generic example: React.Component

You probably now how to declare a React Component in ES6:

class MyComponent extends React.Component {}

With TypeScript, the React.Component class refer to Component type.

P stands for Props type, and S for State type

Component type extends ComponentLifecycle — which provide all famous lifecycle methods:

You can see here how React use Generics to propagate Props and State types to class methods

So, if you use React with TypeScript, remember to provide type arguments for Props and State!

interface Props { user: User }
interface State {}
class MyComponent extends React.Component<Props, State> {
state: State = {}; // important!

// ...

Overloads example: lodash _.filter()

_.filter can take different types of arguments as value, which result in a pretty complex mix of Generic with Overloads typing.

All lodash functions provides this advanced and complete typing!

I hope you now understand the concepts of Generic and Overloads and especially why it’s important is real-world TypeScript project.

If you want to dive further and level-up your typing skills, I advice you to take a look at the following typings :

The next chapter —“Typescript — Super-types 💪”— will cover more complex cases with more real world examples 👷