Generics

Getting to Know Generics

Generics allows you to “parameterize” your type. Let’s see an example to understand what it means.

Consider the following forEach function.

ts
const forEach = (array: any[], callBack: (element: any) => void) => {
for (let item of array) {
callBack(item);
}
};
const numbers = [1, 2, 3];
forEach(numbers, (item) => {
console.log(item.toFixed(2));
console.log(item.padStart()); // runtime error
});
ts
const forEach = (array: any[], callBack: (element: any) => void) => {
for (let item of array) {
callBack(item);
}
};
const numbers = [1, 2, 3];
forEach(numbers, (item) => {
console.log(item.toFixed(2));
console.log(item.padStart()); // runtime error
});

You can, of course, create a wrapper of forEach:

ts
const forEach = (array: any[], callBack: (element: any) => void) => {
for (let item of array) {
callBack(item);
}
};
const forEachNumber = (array: number[], callBack: (element: number) => void) =>
forEach(array, callBack);
const numbers = [1, 2, 3];
forEachNumber(numbers, (item) => {
console.log(item.toFixed(2));
console.log(item.padStart()); // type error
});
ts
const forEach = (array: any[], callBack: (element: any) => void) => {
for (let item of array) {
callBack(item);
}
};
const forEachNumber = (array: number[], callBack: (element: number) => void) =>
forEach(array, callBack);
const numbers = [1, 2, 3];
forEachNumber(numbers, (item) => {
console.log(item.toFixed(2));
console.log(item.padStart()); // type error
});

But that will be painful when you have to create a wrapper for each data type, when there are zero behavior improvement.

What you want is a way to say, whatever type of the item in the array will be the type of the element passed to callback.

Generics allows us to do so:

ts
const forEach = <Item,>(array: Item[], callBack: (element: Item) => void) => {
for (let item of array) {
callBack(item);
}
};
const numbers = [1, 2, 3];
forEach(numbers, (item) => {
console.log(item.toFixed(2));
console.log(item.padStart()); // type error, yay!
});
ts
const forEach = <Item,>(array: Item[], callBack: (element: Item) => void) => {
for (let item of array) {
callBack(item);
}
};
const numbers = [1, 2, 3];
forEach(numbers, (item) => {
console.log(item.toFixed(2));
console.log(item.padStart()); // type error, yay!
});

Item in the example above is called generics, it allows us to provide a “placeholder” for our type, so we can specify the relationship between multiple variables.

Other Generics Format

Generics are not only used for function. We can used it for other TypeScript syntax too.

ts
/* Using generics in class */
// class Queue<T> {
// private data: T[] = [];
// push(item: T) {
// this.data.push(item);
// }
// pop() {
// return this.data.shift();
// }
// }
// const queue = new Queue<number>();
// queue.push(3);
// queue.push('three'); // Error
/* Using generics in type alias */
// type ValueWithId<ValueType> = {
// value: ValueType;
// id: string;
// };
// const numberObject: ValueWithId<number> = {
// value: 5,
// id: 'five',
// };
// const stringObject: ValueWithId<string> = {
// value: 'hello',
// id: 'h',
// };
/* Using generics with interface */
// interface Config<Type> {
// type: Type;
// required: boolean;
// }
// interface SpecialConfig extends Config<'special'> {
// name: string;
// }
// const myConfig: SpecialConfig = {
// type: 'special',
// required: false,
// name: 'Special One',
// };
ts
/* Using generics in class */
// class Queue<T> {
// private data: T[] = [];
// push(item: T) {
// this.data.push(item);
// }
// pop() {
// return this.data.shift();
// }
// }
// const queue = new Queue<number>();
// queue.push(3);
// queue.push('three'); // Error
/* Using generics in type alias */
// type ValueWithId<ValueType> = {
// value: ValueType;
// id: string;
// };
// const numberObject: ValueWithId<number> = {
// value: 5,
// id: 'five',
// };
// const stringObject: ValueWithId<string> = {
// value: 'hello',
// id: 'h',
// };
/* Using generics with interface */
// interface Config<Type> {
// type: Type;
// required: boolean;
// }
// interface SpecialConfig extends Config<'special'> {
// name: string;
// }
// const myConfig: SpecialConfig = {
// type: 'special',
// required: false,
// name: 'Special One',
// };

Note that for the Wrapped and Config example above, the generics allow us to conveniently create some special type based on our use case.

An example of this is the Promise type that is included in TypeScript.

ts
const getJokeText = (): Promise<string> =>
fetch('https://icanhazdadjoke.com/', {
headers: {
Accept: 'text/plain',
},
}).then((res) => res.text());
getJokeText().then((joke) => console.log(joke));
type Joke = {
id: string;
joke: string;
status: number;
};
const getJoke = (): Promise<Joke> =>
fetch('https://icanhazdadjoke.com/', {
headers: {
Accept: 'application/json',
},
}).then((res) => res.json());
getJoke().then((response) => console.log(response.joke));
ts
const getJokeText = (): Promise<string> =>
fetch('https://icanhazdadjoke.com/', {
headers: {
Accept: 'text/plain',
},
}).then((res) => res.text());
getJokeText().then((joke) => console.log(joke));
type Joke = {
id: string;
joke: string;
status: number;
};
const getJoke = (): Promise<Joke> =>
fetch('https://icanhazdadjoke.com/', {
headers: {
Accept: 'application/json',
},
}).then((res) => res.json());
getJoke().then((response) => console.log(response.joke));

Do It: Using Generics

Rename lib/array.js to lib/array.ts.

  1. Annotate includes function
  2. Annotate map function

Challenge: Annotate flattenArray function