Generics can be used in conjunction with functions (creating generic function), classes (creating generic class), and interfaces (creating generic interface).
Basic usage
You have probably used generics in the past even without knowing that - the most common usage of generics is declaring an array:
const myArray: string\[\];
It’s not too special at first glance, we just declaring myArray
as an array of strings, but it is the same as a generic declaration:
const myArray: Array<string\>;
Keepin’ things explicit
Let’s start with very simple example - how could we port this vanilla JS function to TypeScript:
function getPrefiledArray(filler, length) {
return (new Array(length)).fill(filler);
}
This function will return array filled with given amount of filler
, so length
will be number
and whole function will return array of filler
- but what is filler? At this point it can be anything so one option is to use any
:
function getPrefiledArray(filler: any, length: number): any\[\] {
return (new Array(length)).fill(filler);
}
Using any
is certainly generic - we can pass literally anything, so “work with a number of types instead of a single type” from the definition is fully covered, but we lose the connection between filler
type and the return type. In this case, we want to return some common thing, and we can explicitly define this common thing as type parameter:
function getPrefiledArray<T>(filler: T, length: number): T\[\] {
return (new Array(length)).fill(filler);
}
and use like this:
const prefilledArray = getPrefiledArray<number\>(0, 10);
Generic Constraints
Let’s look at different, probably more common cases. Why do we actually use types in functions? For me, it is to ensure that arguments passed to the function will have some properties that I want to interact with.
Once again let’s try to port simple vanilla JS function to TS.
function getLength(thing) {
return thing.length;
}
We have a nontrivial conundrum - how to ensure that the thing has a length
property, and first thought may be to do something like:
function getLength(thing: typeof Array):number {
return thing.length;
}
and depending on the context it might be correct, overall we are a bit generic - it’ll work with arrays of multiple types, but what if we don’t really know if the thing should always be an array - maybe the thing is a football field or a banana peel? In this case, we have to collect the common properties of that thing into a construct that can define properties of an object - an interface:
interface IThingWithLength {
length: number;
}
We can use IThingWithLength
interface as type of the thing
parameter:
function getLength(thing: IThingWithLength):number {
return thing.length;
}
quite frankly in this simple example, it’ll be perfectly fine, but if we want to keep this type generic, and not face the issue from the first example we can use Generic Constraints:
function getLength<T extends IThingWithLength>(thing: T):number {
return thing.length;
}
and use it:
interface IBananaPeel {
thickness: number;
length: number;
}
const bananaPeel: IBananaPeel = {thickness: 0.2, length: 3.14};
getLength(bananaPeel);
Using extends
ensures that T
will contain properties that are defined by IThingWithLength
.
Generic Classes
Up to this point, we were working with generic functions, but it’s not the only place where generics shine, let’s see how can we incorporate them into classes.
First of all let’s try to store bunch of bananas in the bananas basket:
class Banana {
constructor(
public length: number,
public color: string,
public ionizingRadiation: number
) {}
}
class BananaBasket {
private bananas: Banana\[\] = \[\];
add(banana: Banana): void {
this.bananas.push(banana);
}
}
const bananaBasket = new BananaBasket();
bananaBasket.add(new Banana(3.14, ‘red’, 10e-7));
Now let’s try to create general purpose basket, to different stuff with the same type:
class Basket<T\> {
private stuff: T\[\] = \[\];
add(thing: T): void {
this.stuff.push(thing);
}
}
const bananaBasket = new Basket<Banana>();
And lastly, let's assume that our basket is a radioactive material container and we can only store matter that has ionizingRadiation
property:
interface IRadioactive {
ionizingRadiation: number;
}
class RadioactiveContainer<T extends IRadioactive> {
private stuff: T\[\] = \[\];
add(thing: T): void {
this.stuff.push(thing);
}
}
Generic Interface
Lastly let’s try to gather all our knowledge and build radioactive empire also using Generic Interfaces:
interface IRadioactive {
ionizingRadiation: number;
}
interface IBanana extends IRadioactive {
length: number;
color: string;
}
interface IDog {
weight: number;
}
interface IRadioactiveContainer<T extends IRadioactive\> {
add(thing: T): void;
getRadioactiveness():number;
}
class RadioactiveContainer<T extends IRadioactive\> implements IRadioactiveContainer<T\> {
private stuff: T\[\] = \[\];
add(thing: T): void {
this.stuff.push(thing);
}
getRadioactiveness(): number {
return this.stuff.reduce((a, b) => a + b.ionizingRadiation, 0)
}
}
const dogsContainer = new RadioactiveContainer<IDog>();
const radioactiveContainer = new RadioactiveContainer<IRadioactive>();
const bananasContainer = new RadioactiveContainer<IBanana>();
That’s all folks!
Read more:
- Time for a new reality. An era of remote work has started a month ago
- 5 reasons why you will find qualified Ruby developers in Poland
- Web App Development: Why is Ruby on Rails a technology worth choosing?
- 5 reasons why you will find qualified Ruby developers in Poland