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 straightforward 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.

https://www.typescriptlang.org/docs/handbook/generics.html

As explained this excerpt of the TypeScript documentation, we want to create functions that accept many kinds 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, the 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”

⠀

The 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 fulfill 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: