Blogs
Generics in TypeScript

Generics

Generics allows us to define placeholder types which are then replaced when the code is executed with the actual types passed in.

Let’s consider a simple generics before diving into more complex generics:

  • <Input, Output>: These are generic types. Input represents the type of elements in the input array, and Output represents the type of elements in the output array.
  • arr: Input[]: This is the input array, containing elements of type Input.
  • func: (arg: Input) => Output: This is a function that takes an element of type Input and returns a value of type Output.
  • Output[]: This is the return type, an array containing elements of type Output.
Typescript
1
function map<Input, Output>(arr: Input[], func: (arg: Input) => Output): Output[] {
2
return arr.map(func);
3
}
4
5
const parsed = map(["1", "2", "3"], (n) => parseInt(n));
6
7
console.log(parsed) // [ 1, 2, 3 ]

Writing utility functions

Let’s try writing a few common utility functions: map() filter() reduce()

But, you have a constraint which it must only be applicable to an Index Signatures:

Typescript
1
interface Dict<T> {
2
[k: string]: T;
3
}

Say you’re given this input:

Typescript
1
const fruits = {
2
apple: { color: "red", mass: 100 },
3
grape: { color: "red", mass: 5 },
4
banana: { color: "yellow", mass: 183 },
5
lemon: { color: "yellow", mass: 80 },
6
pear: { color: "green", mass: 178 },
7
orange: { color: "orange", mass: 262 },
8
raspberry: { color: "red", mass: 4 },
9
cherry: { color: "red", mass: 5 },
10
};

What if you want to attach a kg and name prop to it?

Let’s start with our map() function:

Typescript
1
function mapDict<T, S>(inputDict: Dict<T>, mapFunction: (original: T, key: string) => S): Dict<S> {
2
const outDict: Dict<S> = {};
3
for (let k of Object.keys(inputDict)) {
4
const val = inputDict[k];
5
outDict[k] = mapFunction(val, k);
6
}
7
return outDict;
8
}

Let’s break it down:(hover:one)

  • <T, S>: These are generic types. T represents the type of values in the inputDict, and S represents the type of values in the resulting dictionary.
  • inputDict: Dict<T>: This is the input dictionary, where keys are strings and values are of type T.
  • mapFunction: (original: T, key: string) => S: This is a function that takes a value of type T and its corresponding key, and returns a new value of type S.
  • Dict<S>: This is the return type, a dictionary with the same keys as inputDict, but with values of type S.
Typescript
1
function mapDict<T, S>(
2
inputDict: Dict<T>,
3
mapFunction: (original: T, key: string) => S
4
): Dict<S>

Once called:

Typescript
1
const fruitsWithKgMass = mapDict(fruits, (fruit, name) => ({ ...fruit, kg: 0.001 * fruit.mass, name }));
2
3
console.log(fruitsWithKgMass);
terminal
1
{
2
apple: { color: 'red', mass: 100, kg: 0.1, name: 'apple' },
3
grape: { color: 'red', mass: 5, kg: 0.005, name: 'grape' },
4
banana: { color: 'yellow', mass: 183, kg: 0.183, name: 'banana' },
5
lemon: { color: 'yellow', mass: 80, kg: 0.08, name: 'lemon' },
6
pear: { color: 'green', mass: 178, kg: 0.178, name: 'pear' },
7
orange: { color: 'orange', mass: 262, kg: 0.262, name: 'orange' },
8
raspberry: { color: 'red', mass: 4, kg: 0.004, name: 'raspberry' },
9
cherry: { color: 'red', mass: 5, kg: 0.005, name: 'cherry' }
10
}

Comprehend what each type is. Once you’ve understood the generics given to you thus far, the rest of the functions are identical:

Typescript
1
function filterDict<T>(inputDict: Dict<T>, filterFunction: (value: T, key: string) => boolean): Dict<T> {
2
const outDict: Dict<T> = {};
3
for (let key of Object.keys(inputDict)) {
4
const val = inputDict[key];
5
if (filterFunction(val, key))
6
outDict[key] = val;
7
}
8
return outDict;
9
}
10
11
function reduceDict<T, S>(
12
inputDict: Dict<T>,
13
reducerFunction: (
14
currentVal: S,
15
dictItem: T,
16
key: string
17
) => S,
18
initialValue: S
19
): S {
20
let value = initialValue
21
for (let k of Object.keys(inputDict)) {
22
const thisVal = inputDict[k]
23
value = reducerFunction(value, thisVal, k)
24
}
25
return value
26
}
27
28
const redFruits = filterDict(fruits, (fruit) => fruit.color === 'red');
29
const oneOfEachFruitMass = reduceDict(fruits, (currentMass, fruit) => currentMass + fruit.mass, 0);

You can also use Alias:

Typescript
1
// Define the type for FruitType
2
type FruitType = {
3
color: string;
4
mass: number;
5
};
6
7
// Using mapDict to add 'kg' mass and 'name' to each fruit
8
const fruitsWithKgMass = mapDict<FruitType, FruitType & { kg: number; name: string }>(
9
fruits,
10
(fruit, name) => ({
11
...fruit,
12
kg: 0.001 * fruit.mass, // Convert mass to kilograms
13
name, // Add the name of the fruit
14
})
15
);

More Generics

Constraints

Let’s write a function that returns the longer of two values. To do this, we need a length property that’s a number. We constrain the type parameter to that type by writing an extends clause:

Typescript
1
function longest<Type extends { length: number }>(a: Type, b: Type): Type {
2
if (a.length >= b.length) {
3
return a;
4
} else {
5
return b;
6
}
7
}
8
9
function longTwo<T extends { length: number }>(a: T, b: T): T {
10
if (a.length == 1) {
11
return a;
12
} else {
13
return b;
14
}
15
}
16
17
const longerArray = longest([1, 2], [1, 2, 3]);
const longerArray: number[]
18
19
const notOK = longest(10, 100); // Error, it does not meet the constraint!
const notOK: { length: number; }
20
21
const longStr = longTwo("aa", "b"); // b
const longStr: "aa" | "b"

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:

Typescript
1
function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
2
return obj[key];
3
}
4
5
let x = { a: 1, b: 2, c: 3, d: 4 };
6
7
getProperty(x, "a");
8
getProperty(x, "m");

Using interfaces:

Typescript
1
interface Lengthwise {
2
length: number;
3
}
4
5
function loggingIdentity<Type extends Lengthwise>(arg: Type): Type {
6
console.log(arg.length); // Now we know it has a .length property, so no more error
7
return arg;
8
}

More functions? No problem:

Typescript
1
interface GenericIdentityFn<Type> {
2
(arg: Type): Type;
3
}
4
5
function identity<Type>(arg: Type): Type {
6
return arg;
7
}
8
9
let myIdentity: GenericIdentityFn<number> = identity;
10
11
myIdentity(1); // ok
12
myIdentity("1"); // not ok

Take a fetch function example:

Typescript
1
interface ObjOne {
2
prop1: string;
3
prop2: number;
4
}
5
6
interface ObjTwo {
7
prop3: string;
8
prop4: string;
9
}
10
11
// Defining our function with a generic type (T)
12
async function fetchData<T>() {
13
const response = await fetch("API_URL");
14
// Telling TS that the response data is the type of T (our generic)
15
const data = (await response.json()) as T;
16
17
return data;
18
}
19
20
await fetchData<ObjOne>(); // The returned data would have a type of ObjOne
21
await fetchData<ObjTwo>(); // The returned data would have a type of ObjTwo
Extracting Keys from an Object

We can extract keys from an object by using default private generics. Hence, _ExtractedKeys is set to a default contraint.

Typescript
1
type Obj = {
2
a: "a",
3
b: "b",
4
aTwo: "another a"
5
bTwo: "another b"
6
}
7
8
type ValuesOfKeysStartingWIthA<Obj, _ExtractedKeys extends keyof Obj = Extract<keyof Obj, `a${string}`>> = {
9
[K in _ExtractedKeys]: Obj[K]
10
}[_ExtractedKeys];
11
12
type NewUnion = ValuesOfKeysStartingWIthA<Obj>;
type NewUnion = "a" | "another a"

Generic Classes

Here is a blueprint of a generic class:

Typescript
1
class GenericClass<T> {
2
// ...
3
4
thisIsAProp: (x: T, y: T) => T; // prop
5
6
thisIsAMethod(x: T): T { // method
7
return x;
8
}
9
10
thisIsArrowFunc = (x: T, y: T) => { // arrow func
11
console.log();
12
}
13
}

Let’s build a minimal generic class:

Typescript
1
class ExampleClass<T> {
2
private values: T[] = [];
3
4
setValue(value: T): void {
5
this.values.push(value);
6
}
7
8
getValues(): T[] {
9
return this.values;
10
}
11
}
12
13
const example = new ExampleClass<number>();
14
example.setValue(24);
15
example.setValue(42);
16
17
const values: number[] = example.getValues();
[24, 42]

Example of Using Class Types in Generics:

Typescript
1
function create<T>(c: { new (): T }): T {
2
return new c();
3
}

This is a minimal factory method, the creattion is possible due to the Bee and Lion meeting the constraint.

Typescript
1
class BeeKeeper {
2
hasMask: boolean = true;
3
}
4
5
class ZooKeeper {
6
nametag: string = "Mikle";
7
}
8
9
class Animal {
10
numLegs: number = 4;
11
}
12
13
class Bee extends Animal {
14
numLegs = 6;
15
keeper: BeeKeeper = new BeeKeeper();
16
}
17
18
class Lion extends Animal {
19
keeper: ZooKeeper = new ZooKeeper();
20
}
21
22
function createInstance<A extends Animal>(c: new () => A): A {
23
return new c();
24
}
25
26
const objOne = createInstance(Lion).keeper;
const objOne: ZooKeeper
27
const objTwoHasMask = createInstance(Bee).keeper.hasMask;
const objTwoHasMask: boolean

Using generics with interfaces

As shown before, we can use generics with interfaces to define custom types for the properties of the object the interface describes. Here are more examples:

Typescript
1
interface MetaData {
2
sex: string;
3
height: "tall" | "short";
4
favouriteNumber: number;
5
}
6
7
// Defining our generic
8
interface Person<T> {
9
id: number;
10
name: string;
11
age: number;
12
metadata: T;
13
}
14
15
// Using our generic
16
const personOne: Person<(number|string)[]> = {
17
id: 1,
18
name: 'Jeff',
19
age: 31,
20
metadata: ['male', 'tall', 22]
21
}
22
23
// Using our generic
24
const personTwo: Person<MetaData> = {
25
id: 1,
26
name: 'Jeff',
27
age: 31,
28
metadata: {
29
sex: 'female',
30
height: 'tall',
31
favouriteNumber: 45,
32
}
33
}

the same applies to alias:

Typescript
1
type MetaData = {
2
sex: string;
3
height: "tall" | "short";
4
favouriteNumber: number;
5
}
6
7
type Person<T> = {
8
id: number;
9
name: string;
10
age: number;
11
metadata: T;
12
}
13
14
const personOne: Person<(number|string)[]> = {
15
id: 1,
16
name: 'Jeff',
17
age: 31,
18
metadata: ['male', 'tall', 22]
19
}
20
21
const personTwo: Person<MetaData> = {
22
id: 2,
23
name: 'Jess',
24
age: 28,
25
metadata: {
26
sex: 'female',
27
height: 'tall',
28
favouriteNumber: 45,
29
}
30
}
Dynamic function arguments with Generics

Consider the following:

This type Event can be inferred as either LOG_IN wiht its optional payload or SIGN_OUT. The problem here is that TS is accepting this, so there is no strict rules telling us that we can pass, say sendEvent("SIGN_OUT", {});. Take a moment to read the code:

Typescript
1
export type Event =
2
| {
3
type: "LOG_IN";
4
payload: {
5
userId: string;
6
};
7
}
8
| {
9
type: "SIGN_OUT";
10
};
11
12
const sendEvent = (eventType: Event["type"], payload?: any) => {};
13
14
// Correct
15
sendEvent("SIGN_OUT");
16
sendEvent("LOG_IN", { userId: "123" });
17
18
// Wrong - passing incorrect/invalid payload
19
sendEvent("SIGN_OUT", {});
20
sendEvent("LOG_IN", { userId: 123 });
21
sendEvent("LOG_IN", {});
22
sendEvent("LOG_IN");

To fix this, we can once again use Generics. It helps ensure that the function’s arguments are type-safe based on the event type passed to it.

  • ...args: This uses the rest parameter syntax, allowing the function to accept a variable number of arguments. The type of args is determined conditionally based on the Type value.

  • Extract<Event, { type: Type }>: This extracts the subset of the Event union type where the type matches the generic Type. This way, we can narrow down the specific event type from a union of event types.

extends { payload: infer TPayload } ? [type: Type, payload: TPayload] : [type: Type]: This checks if the extracted event type has a payload property. If it does (extends { payload: infer TPayload }), the function expects two arguments: the type and the payload. If it doesn’t, the function expects only one argument: the type.

Typescript
1
const sendEvent = <Type extends Event["type"]>(
2
...args: Extract<Event, { type: Type }> extends { payload: infer TPayload }
3
? [type: Type, payload: TPayload]
4
: [type: Type]
5
) => {};

You can also do more cool stuff such as a key remover:

Typescript
1
export const makeKeyRemover =
2
<Key extends string>(keys: Key[]) =>
3
<Obj>(obj: Obj): Omit<Obj, Key> => {
4
return {} as any;
5
};
6
7
const keyRemover = makeKeyRemover(['a']);
8
9
const newObject = keyRemover({
const newObject: Omit<{ a: number; b: number; c: number; }, "a">
10
a: 1,
11
b: 2,
12
c: 3,
13
});
14
15
newObject.b
16
newObject.c

Exercise: Convert Classess to Generic Classes

My challenge to you is to convert this code to using Generics using T as your type.

Hint: Start by typing InMemoryDatabase<T>

1
interface Database {
2
get(id: string): string;
3
set(id: string, value: string): void;
4
}
5
6
interface Persistable {
7
saveToString(): string;
8
restoreFromString(storedState: string): void;
9
}
10
11
class InMemoryDatabase implements Database {
12
protected db: Record<string, string> = {};
13
14
get(id: string): string {
15
return this.db[id];
16
}
17
18
set(id: string, value: string): void {
19
this.db[id] = value;
20
}
21
}
22
23
class PersistantMemoryDB extends InMemoryDatabase implements Persistable {
24
saveToString(): string {
25
return JSON.stringify(this.db);
26
}
27
restoreFromString(storedState: string): void {
28
this.db = JSON.parse(storedState);
29
}
30
}

Solution:

Typescript
1
interface Database<T> {
2
get(id: string): T;
3
set(id: string, value: T): void;
4
}
5
6
interface Persistable {
7
saveToString(): string;
8
restoreFromString(storedState: string): void;
9
}
10
11
class InMemoryDatabase<T> implements Database<T> {
12
protected db: Record<string, T> = {};
13
14
get(id: string): T {
15
return this.db[id];
16
}
17
18
set(id: string, value: T): void {
19
this.db[id] = value;
20
}
21
}
22
23
class PersistantMemoryDB<T> extends InMemoryDatabase<T> implements Persistable {
24
saveToString(): string {
25
return JSON.stringify(this.db);
26
}
27
restoreFromString(storedState: string): void {
28
this.db = JSON.parse(storedState);
29
}
30
}
31
32
const myDb = new PersistantMemoryDB<number>();
33
myDb.set("123", 1);

Exercise: Remove Prefix

Consider removing the url prefix. The type DesiredShape should return a type without the prefix url.

Typescript
1
interface ApiData {
2
"url:some-link": string;
3
"url:some-link-two": string;
4
}
5
6
type RemoveUrlFromObj<T> = {
7
[K in keyof T]: T[K]
8
}
9
type DesiredShape = RemoveUrlFromObj<ApiData>;
type DesiredShape = { "url:some-link": string; "url:some-link-two": string; }

Solution:

Typescript
1
interface ApiData {
2
"url:some-link": string;
3
"url:some-link-two": string;
4
}
5
6
type RemoveUrls<T> = T extends `url:${infer U}` ? U : T;
7
8
type RemoveUrlFromObj<T> = {
9
[K in keyof T as RemoveUrls<K>]: T[K]
10
}
11
type DesiredShape = RemoveUrlFromObj<ApiData>;
type DesiredShape = { "some-link": string; "some-link-two": string; }