📜 ⬆️ ⬇️

Generics in TypeScript: sorting out together

Hello! The TestMace team publishes a regular translation of an article from the world of web development. This time for newbies! Enjoy reading.


Dispel the veil of mystery and misunderstanding over the syntax <T> and finally make friends with it



Probably only experienced developers of Java or other strongly typed languages ​​do not blink when they see a generic in TypeScript. Its syntax is fundamentally different from all that we used to see in JavaScript, so it’s so difficult to guess what it does at all.


I would like to show you that everything is actually much simpler than it seems. I will prove that if you are able to implement a function with arguments in JavaScript, then you can use generics without any extra effort. Go!


Generics in TypeScript


The TypeScript documentation provides the following definition: "generics are the ability to create components that work not only with one, but with several data types."


Great! So, the basic idea is that generics allow us to create some reusable components that work with different types of data transferred to them. But how is this possible? That's what I think.


Generics and types are related to each other, like the values ​​and arguments of a function. This is such a way to tell components (functions, classes or interfaces) what type you need to use when calling them, just like during a call we tell functions which values ​​to use as arguments.


It is best to analyze this by the example of a generic identity function. The identity function is a function that returns the value of the argument passed to it. In javascript it will look like this:


identity.js
 function identity (value) { return value; } console.log(identity(1)) // 1 

Let's make it work with numbers:


identity.ts
 function identity (value: Number) : Number { return value; } console.log(identity(1)) // 1 

Well, we added a type to the definition of the identical function, but we would like it to be more flexible and work for values ​​of any type, not just numbers. This is what the generics are for. They allow functions to take values ​​of any data type at the input and, depending on them, convert the function itself.


genericIdentity.ts
 function identity <T>(value: T) : T { return value; } console.log(identity<Number>(1)) // 1 

Oh, this weird <T> syntax! Set aside the panic. We just pass the type we want to use for a particular function call.



Look at the picture above. When you call identity<Number>(1) , the Number type is the same argument as 1. It is substituted instead of T everywhere. A function can take several types in the same way that it takes several arguments.



Look at the function call. Now, the generics syntax shouldn't scare you. T and U are simply the names of the variables that you assign yourself. When calling a function, the types with which the function will operate are indicated instead .


An alternative version of understanding the concept of generics is that they transform the function depending on the specified data type. The animation below shows how the function's entry changes and the result returned when the type changes.



As you can see, the function accepts any type, which allows you to create reusable components of various types, as promised in the documentation.


Pay particular attention to the second console.log call on the animation above - the type is not passed to it. In this case, TypeScript will attempt to compute the type from the transferred data.


Generic Classes and Interfaces


You already know that generics are just a way to transfer types to a component. You have just seen how they work with functions, and I have good news: they work with classes and interfaces in exactly the same way. In this case, an indication of the types follows the name of the interface or class.


Look at the example and try to figure it out for yourself. I hope you did it.


genericClass.ts
 interface GenericInterface<U> { value: U getIdentity: () => U } class IdentityClass<T> implements GenericInterface<T> { value: T constructor(value: T) { this.value = value } getIdentity () : T { return this.value } } const myNumberClass = new IdentityClass<Number>(1) console.log(myNumberClass.getIdentity()) // 1 const myStringClass = new IdentityClass<string>("Hello!") console.log(myStringClass.getIdentity()) // Hello! 

If the code is not immediately clear, try to track the type from top to bottom down to function calls. The procedure is as follows:


  1. A new instance of the IdentityClass class is created, and the type of Number and the value 1 are passed to it.
  2. In the class, the value T assigned the type Number .
  3. IdentityClass implements GenericInterface<T> , and we know that T is Number , and this record is equivalent to writing GenericInterface<Number> .
  4. In GenericInterface generic U becomes Number . In this example, I intentionally used different variable names to show that the value of the type goes up the chain, and the name of the variable has no value.

Real use cases: go beyond primitive types


In all the above code inserts, primitive types like Number and string were used. For examples, the fact is, but in practice you are unlikely to begin using generics for primitive types. Generics will be truly useful when working with arbitrary types or classes that form an inheritance tree.


Consider the classic example of inheritance. Suppose we have a class Car , which is the basis of the classes Truck and Vespa . We write the service function washCar , which takes a generic instance of Car and returns it.


car.ts
 class Car { label: string = 'Generic Car' numWheels: Number = 4 horn() { return "beep beep!" } } class Truck extends Car { label = 'Truck' numWheels = 18 } class Vespa extends Car { label = 'Vespa' numWheels = 2 } function washCar <T extends Car> (car: T) : T { console.log(`Received a ${car.label} in the car wash.`) console.log(`Cleaning all ${car.numWheels} tires.`) console.log('Beeping horn -', car.horn()) console.log('Returning your car now') return car } const myVespa = new Vespa() washCar<Vespa>(myVespa) const myTruck = new Truck() washCar<Truck>(myTruck) 

By washCar function that T extends Car , we denote which functions and properties we can use inside this function. Generic also allows you to return data of the specified type instead of the usual Car .


The result of this code will be:


 Received a Vespa in the car wash. Cleaning all 2 tires. Beeping horn - beep beep! Returning your car now Received a Truck in the car wash. Cleaning all 18 tires. Beeping horn - beep beep! Returning your car now 

Let's sum up


I hope I helped you deal with generics. Remember, all you have to do is just pass the type value to the function :)


If you want to read more about generics, I attached a couple of links further.


What to read :



Our team creates a cool TestMace tool - a powerful IDE for working with APIs. Create scripts, test endpoints and use all the power of advanced autocompletion and syntax highlighting. Write to us! We are here: Telegram , Slack , Facebook , Vk


')

Source: https://habr.com/ru/post/455473/


All Articles