Generics in TypeScript: Getting Together

Original author: Jim Rottinger
  • Transfer

Hello! TestMace team publishes another translation of the article from the world of web development. This time for beginners! Enjoy reading.


Shatter the veil of mystery and misunderstanding of the syntax and finally make friends with it



Probably, only seasoned developers of Java or other strongly typed languages ​​do not bang their eyes when they see the generic in TypeScript. Its syntax is fundamentally different from everything that we are used to seeing in JavaScript, so it's not so easy to immediately guess what it does at all.


I would like to show you that in fact, everything is 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 multiple data types."


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


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


It is best to understand this using the generic of an identical function as an example. An identical 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 an identical function, but we would like it to be more flexible and work for values ​​of any type, and not just numbers. This is what generics are for. They allow the function to take values ​​of any type of data at the input and, depending on them, transform the function itself.


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

Oh, that weird syntax ! Stop the panic. We just pass the type that we want to use for a specific function call.



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



Look at the function call. Now the generic syntax should not scare you. Tand U- these are just the names of the variables that you assign yourself. When a function is called, the types with which this function will work 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 record and the returned result change when the type is changed.



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 special attention to the second call to console.log in the animation above - the type is not passed to it. In this case, TypeScript will try to calculate the type from the transmitted data.


Generic Classes and Interfaces


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


Look at an example and try to figure it out for yourself. I hope you succeeded.


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

If the code is not immediately understood, try to track the values typefrom top to bottom until the function calls. The procedure is as follows:


  1. A new instance of the class is created IdentityClass, and the type Numberand value are passed to it 1.
  2. In the class, the value is Tassigned a type Number.
  3. IdentityClassimplements , and we know that - this , and such a record is equivalent to a record .GenericInterfaceTNumberGenericInterface
  4. In GenericInterfacegeneric Ubecomes Number. In this example, I intentionally used different variable names to show that the type value goes up the chain, and the variable name has no meaning.

Real use cases: go beyond primitive types


In all the above code inserts, primitive types like Numberand were used string. For examples, this is the most, but in practice, you are unlikely to use generics for primitive types. Generics will be really useful when working with arbitrary types or classes that form an inheritance tree.


Consider a classic example of inheritance. Suppose we have a class Carthat is the basis of Truckand Vespa. We write a service function washCarthat takes a generic instance Carand 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  (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(myVespa)
const myTruck = new Truck()
washCar(myTruck)

By telling the function washCarthat 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 one 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

To summarize


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


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


What to read :



Also popular now: