Consider a simple class that implements the queue data structure. It has an array where it stores the data, a method to push a new item into the queue, and a method to pop an existing item from the queue.
An issue with this implementation is that there is nothing constraining the items that are pushed to the queue to match the items that are popped from the queue. Let's demonstrate this by creating a queue.
You can push a variable of any type onto the queue, for example, a number, or by mistake, you can even push a string. Later on, when you pop an item, the developer might think it is always a number. It is safe to call, for example, the two preceding function on the popped item, but then you get runtime errors.
Such errors can be hard to track down in a large codebase. One solution, and in fact the only solution for languages that don't support generics is to go ahead and create special classes just for these constraints.
For example, a quick and dirty number queue would extend the queue and enforce the constraint on the pushed item to match the value that is returned when the item is popped. Internally, it'll just call the super-implementation.
Now, if you use a specialized data structure class, pushed items will be limited to the correct data type. Mistakes will be caught at compile time. For example, if you try to push in a string, it's going to complain, and you can fix these mistakes at compile time instead of having runtime errors.
Doing class specialization this way by class inheritance can quickly become painful. For example, if you want a string queue, you have to go through all this effort again. What you really want is a way to say that whatever the type of stuff you push is the same as the type of stuff you pop.
This is why programming languages need generics to allow you to specify such constraints at compile time. Since TypeScript supports generics, we can add a generic parameter to the queue class. You are free to call it whatever you want, but we will call it t-value to indicate that it is a type of the value.
We will constrain the pushed items to match this type, and also constrain the popped items will match this type. Now we can use this generic class instead of having to specialize it. We simply instantiate the generic type when we create the queue. You can see that the type flows through to autocomplete on the pop method as well.
Note that the constraint between the push and the pop methods was there even when we left it as untyped, raw JS. In that case, it was implicit, and you would have to figure out a way to explain it to the user.
For example, here we are creating a queue where each item must have a name property of type string. If you go ahead and push the correct data structure, TypeScript will be very happy with it. However, if you make a typo in the data structure -- for example, misspell the name property --, you will get a nice compile-time error.
Bugs like these can be hard to track down, but with generics, you can offload that work to the compiler. You can also use generics to constrain members of a simple function. For example, a reverse function for an array D will return an array of D. The constraint here is between the function argument and the function return value.
Within the function, you simply clone the array using slice, and return the reversed version of the clone array. Let's go ahead and create an array of objects. Each item in the array will have a name property, which is pointing to a string. We can go ahead and reverse the items in the array using the reverse function.
Because of the generic constraint, TypeScript knows that the reversed array is also an array of similar objects. If you try to push an item with an invalid data structure, you get a nice compiler error. Similarly, if you try to read an invalid property on a popped item, you will get a compiler error. You also get autocomplete for the correct property.