|
| 1 | + |
| 2 | +In this lesson we will talk more about Generic Types. Let's take an array as an example. |
| 3 | + |
| 4 | +An array is a container type that stores values of any specified type inside it. The logic of array operation does not depend on the type of data stored inside. This definition automatically indicates that we are dealing with a generalized type. |
| 5 | + |
| 6 | +To work with such a type, we need to instantiate the internal type at the moment when we want to start working with data of this type: |
| 7 | + |
| 8 | +```typescript |
| 9 | +const numbers: Array<number> = []; |
| 10 | +numbers.push(1); |
| 11 | + |
| 12 | +const strings: Array<string> = []; |
| 13 | +numbers.push('hexlet'); |
| 14 | +``` |
| 15 | + |
| 16 | +The type that is specified inside the angle brackets is called **type parameter**. This name was chosen for a reason - specifying a parameter looks like a function call. Below we will see that this way of looking at generics helps us to better understand how they work. |
| 17 | + |
| 18 | +Let's imagine that we want to define our own collection, which works like an array, but with additional features. Such collections are often made in ORMs to work with data loaded from a database. Let's first describe a specific version of this type that works only with numbers and a couple of standard methods: |
| 19 | + |
| 20 | +```typescript |
| 21 | +type MyColl = { |
| 22 | + data: Array<number>; |
| 23 | + forEach(callback: (value: number, index: number, array: Array<number>) => void): void; |
| 24 | + at(index: number): number | undefined; |
| 25 | +} |
| 26 | +``` |
| 27 | +
|
| 28 | +Here we see that the collection data is stored in a numeric array. There are two methods defined in the type, one of which (`forEach`) passes the elements of the collection to the colback, and the other (`at`) returns the elements of the collection at the specified index. One possible implementation of this type might look like this: |
| 29 | +
|
| 30 | +```typescript |
| 31 | +// The types can be omitted as they are specified in `MyColl` |
| 32 | +const coll: MyColl = { |
| 33 | + data: [1, 3, 8], |
| 34 | + forEach(callback) { |
| 35 | + this.data.forEach(callback); |
| 36 | + }, |
| 37 | + at(index) { |
| 38 | + return this.data.at(index); // target >= ES2022 |
| 39 | + }, |
| 40 | +} |
| 41 | + |
| 42 | +coll.at(-1); // 8 |
| 43 | +``` |
| 44 | + |
| 45 | +Now let's try to generalize this type, i.e. make it generic. To do this, we need to do one simple thing: for elements of the collection, instead of `number` write `T` (or any other name starting with a capital letter) and add `T` as a type parameter to the definition: |
| 46 | + |
| 47 | +```typescript |
| 48 | +type MyColl<T> = { |
| 49 | + data: Array<T>; |
| 50 | + forEach(callback: (value: T, index: number, array: Array<T>) => void): void; |
| 51 | + at(index: number): T | undefined; |
| 52 | +} |
| 53 | +``` |
| 54 | +
|
| 55 | +This type definition can be viewed as a kind of function definition. When a specific type is specified, for example, `MyColl<string>`, then `T` in this situation is replaced by `string` inside the type definition. And if other generics are used inside the type, they "call" the type further. That is, it all works like nested function calls. |
| 56 | +
|
| 57 | +## Limitations of generics |
| 58 | +
|
| 59 | +Generics can have limitations. For example, we can say that the type that is passed to a generic must implement some interface. This is done using the `extends` keyword. Suppose we can make our `MyColl` type work only with types that implement the `HasId` interface: |
| 60 | +
|
| 61 | +```typescript |
| 62 | +interface HasId { |
| 63 | + id: number; |
| 64 | +} |
| 65 | + |
| 66 | +type MyColl<T extends HasId | number> = { |
| 67 | + data: Array<T>; |
| 68 | + forEach(callback: (value: T, index: number, array: Array<T>) => void): void; |
| 69 | + at(index: number): T | undefined; |
| 70 | +} |
| 71 | +``` |
| 72 | +
|
| 73 | +This allows us to use the `MyColl` type only with types that implement the `HasId` interface. For example, such code will not work: |
| 74 | +
|
| 75 | +```typescript |
| 76 | +const coll: MyColl<number> = { |
| 77 | + data: [1, 3, 8], |
| 78 | + forEach(callback) { |
| 79 | + this.data.forEach(callback); |
| 80 | + }, |
| 81 | + at(index) { |
| 82 | + return this.data.at(index); // target >= ES2022 |
| 83 | + }, |
| 84 | +} |
| 85 | +``` |
| 86 | + |
| 87 | +Generics themselves are found everywhere in the code of libraries and frameworks. For example, in `React` component types are wrapped in generics so that you can specify props types. Generics can be used to create more generic types that can work with different data types, which we will explore in the next lessons. |
0 commit comments