Skip to main content

Generics

Generics are used to create reusable components. They are able to work over a variety of types rather than a single one.

An example of a generic is the type Array:

let list: Array<number> = [1, 2, 3];

The Array type is generic, so it works over a range of types rather than a single one. Here, we've told TypeScript that list should be an array of numbers. We could have also created an array of strings, booleans, or any other type.

Generic Types​

Below we define a generic identity function that works over a range of types. The type of this generic function is just like those of a non-generic function, with the type parameters listed first, similarly to function declarations:

function identity<T>(arg: T): T {
return arg;
}

let myIdentity: <T>(arg: T) => T = identity;

We could also have used a different name for the generic type parameter in the type, so long as the number of type variables and how the type variables are used line up.

function identity<Type>(arg: Type): Type {
return arg;
}

let myIdentity: <Input>(arg: Input) => Input = identity;

Generic Interfaces​

The generic type can also be used with the interface. The following is a generic interface.

interface IProcessor<T>
{
result: T;
process(a: T, b: T) => T;
}

The above IProcessor is a generic interface because we used type variable <T>. The IProcessor interface includes the generic field result and the generic method process() that accepts two generic type parameters and returns a generic type.

Another example would be a KeyValuePair interface:

interface KeyPair<T, U> {
key: T;
value: U;
}

let kv1: KeyPair<number, string> = { key: 1, value: "Steve" }; // OK
let kv2: KeyPair<number, number> = { key: 1, value: 12345 }; // OK

Generic Classes​

A generic class has a similar shape to a generic interface. Generic classes have a generic type parameter list in angle brackets (<>) following the name of the class:

class GenericNumber<NumType> {
zeroValue: NumType;
add: (x: NumType, y: NumType) => NumType;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) {
return x + y;
};

We could have instead used string or even more complex objects:

let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function (x, y) {
return x + y;
};

console.log(stringNumeric.add(stringNumeric.zeroValue, "test"));

Generic Constraints​

You may sometimes want to write a generic function that works on a set of types where you have some knowledge about what capabilities that set of types will have.

Consider the following example:

function merge<U, V>(obj1: U, obj2: V) {
return {
...obj1,
...obj2,
};
}

merge() is a generic function that merges two objects like this:

let person = merge({ name: "John" }, { age: 25 });

console.log(result); // { name: 'John', age: 25 }

It works perfectly fine. However, it doesn’t prevent you from passing a non-object like this:

let person = merge({ name: "John" }, 25);

console.log(result); // { name: 'John' }

Instead of working with all types, you may want to add a constraint to the merge() function so that it works with objects only:

function merge<U extends object, V extends object>(obj1: U, obj2: V) {
return {
...obj1,
...obj2,
};
}

Because the merge() function is now constrained, it will no longer work with all types. Instead, it works with the object type only.

Using Type Parameters in Generic Constraints​

We can use the keyof operator to declare a type parameter that is constrained by another type parameter. For example, here we’d like to get a property from an object given its name. We’d like to ensure that we’re not accidentally grabbing a property that does not exist on the obj, so we’ll place a constraint between the two types:

function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
return obj[key];
}

let x = { a: 1, b: 2, c: 3, d: 4 };

getProperty(x, "a");
// Error: "m" does not exist on x
getProperty(x, "m");