Introduction to TypeScript Generics — Classes
Tags: webdev, programming, typescript, javascript
Published: 2021-01-23 20:04:36

**Check out my books on Amazon at https://www.amazon.com/John-Au-Yeung/e/B08FT5NT62**

**Subscribe to my email list now at http://jauyeung.net/subscribe/**

One way to create reusable code is to create code that lets us use it with different data types as we see fit. TypeScript provides the generics construct to create reusable components where we can work with a variety of types with one piece of code. This allows devs to use these components by putting in their own types. In this article, we’ll look at the many ways to define generic classes and also validate properties by defining interfaces and the `extends` keyword and also force the code to only accept property names that are in an object by using the `keyof` keyword.

Defining a Generic Class
========================

To define generic classes in TypeScript, we put the generic type marker in between the `<>` after the class name. We can have more than one type marker separated by a comma. Also, we can use the same type marker to mark the type of the function to let us change parameter and return type of methods within our class. For example, we can write the following code to define a generic class with a single generic type marker:

```
class GenericPerson<T> {    
  name: T;  
  getName: (name: T) => T;  
}let person = new GenericPerson<string>();  
person.name = 'Jane';  
person.getName = function(x) { return x; };
```

In the code above, we passed in the string type inside the `<>` . We can also change to any other type we want like `number`, `boolean`, or any interface like in the following code:

```
class GenericPerson<T> {    
  name: T;  
  getName: (name: T) => T;  
}let person = new GenericPerson<number>();  
person.name = 2;  
person.getName = function(x) { return x; };
```

We can also insert multiple types within the same class as in the following code:

```
class GenericPerson<T, U> {    
  name: T;  
  getName: (name: U) => U;  
}let person = new GenericPerson<string, number>();  
person.name = 'Jane';  
person.getName = function (x) { return x; };  
person.getName(2);
```

In the code above, `T` and `U` may or may not be different types. As long as we put them both in and that we pass in and assign data with the types we specified in the class definition, then the TypeScript compiler will accept it. The examples above are only useful for a limited amount of cases since we can’t reference any properties in our class definition since the TypeScript compiler doesn’t know what properties `T` or `U` contains.

To make the TypeScript compiler aware of the properties that may be used in our class definition, we can use an interface to list all the properties that can be used with the class and the use the `extends` keyword after the generic type marker to denote the that type may have the list of properties that are listed in the interface. For example, if we want to have different complex types in our class methods, we can write something like the following code:

```
interface PersonInterface {  
  name: string;  
  age: number;  
}

interface GreetingInterface {  
  toString(): string;  
}

class GenericPerson<T extends PersonInterface, U extends GreetingInterface> {    
  person: T;  
  greet(greeting: U): string {  
    return `${greeting.toString()} ${this.person.name}. You're ${this.person.age} years old`;  
  }  
}

let jane = new GenericPerson();  
jane.person = {  
  name: 'Jane',  
  age: 20  
};  
console.log(jane.greet('Hi'));
```

In the code above, we defined 2 interfaces, the `PersonInterface` and the `GreetingInterface` to denote the properties that can be referenced with `T` and `U` respectively. In the `PersonInterface`, we have the `name` and `age` properties listed so we can reference these properties for data with type `T`. For the data with `U` type we can call the `toString` method on it. Therefore, in our `greet` method, we can call `toString` on `greeting` since it has the `U` type and since `this.person` has the `T` type, we can get the `name` and `age` properties from it.

Then after the class definition, we can instantiate the class and then set values for the `name` and `age` properties on the `jane` object that we created. Then when we run `console.log` on `jane.greet(‘Hi’)` then we should see ‘Hi Jane. You’re 20 years old’ since we set the values for `jane.person`.

We can also put in the types explicitly when we instantiate the object to make the types more clear. Instead of what we have above, we can change it slightly and write the following code:

```
interface PersonInterface {  
  name: string;  
  age: number;  
}

interface GreetingInterface {  
  toString(): string;  
}

class GenericPerson<T extends PersonInterface, U extends GreetingInterface> {    
  person: T;  
  greet(greeting: U): string {  
    return `${greeting.toString()} ${this.person.name}. You're ${this.person.age} years old`;  
  }  
}

let jane = new GenericPerson<PersonInterface, string>();  
jane.person = {  
  name: 'Jane',  
  age: 20  
};  
console.log(jane.greet('Hi'));
```

The only difference is that we add the `<PersonInterface, string>` after `new GenericPerson`. Note that we can add interfaces or primitive types in between the brackets. TypeScript only cares that the type has the methods listed in the interface we defined. Now that the types are constrained by these generic types, we don’t have to worry about referencing any unexpected properties. For example, if we reference something that doesn’t exist like in the following code:

```
interface PersonInterface {  
  name: string;  
  age: number;  
}

interface GreetingInterface {  
  toString(): string;  
}

class GenericPerson<T extends PersonInterface, U extends GreetingInterface> {    
  person: T;  
  greet(greeting: U): string {  
    return `${greeting.foo()} ${this.person.name}. You're ${this.person.age} years old`;  
  }  
}

let jane = new GenericPerson<PersonInterface, string>();  
jane.person = {  
  name: 'Jane',  
  age: 20  
};  
console.log(jane.greet('Hi'));
```

We would get “Property ‘foo’ does not exist on type ‘U’.(2339)” since we didn’t list `foo` in our `GreetingInterface` .


Constraining Retrieval of Object Properties
===========================================

TypeScript also provides a way to let us get the properties of an object safely with generics. We can use the `keyof` keyword to constrain the values of the than an object can take on to the key names of another object. For example, we can use the `keyof` keyword like in the following code:

```
function getProperty<T, K extends keyof T>(obj: T, key: K) {  
  return obj[key];  
}

let x = { foo: 1, bar: 2, baz: 3 };console.log(getProperty(x, "foo"));
```

In the code above, we constrain the generic marker `K` to only accept the keys of whatever object is passed in to `obj` as the valid values of `key` since `K` has the `extends keyof T` marker after `K` . This means that whatever keys the `T` type has, then the keys are valid values for `K` . With code like the ones above, we don’t have to worry about getting values for properties that don’t exist. So if we pass in a key name that doesn’t exist in `obj` , like in the following code:

```
getProperty(x, "a");
```

Then the TypeScript compiler will reject the code and give the error message “Argument of type ‘“a”’ is not assignable to parameter of type ‘“foo” | “bar” | “baz”’.(2345)“. This means that only `'foo'` , `'bar'` and `'baz'` are valid values for `key` since it has the type `K` , which has the `extends keyof T` marker after it to constrain the valid uses to be the key names of `obj` .

We can define a generic class easily with TypeScript. To define generic classes in TypeScript, we put the generic type marker in between the `<>` after the class name. We can have more than one type marker separated by a comma. Also, we can use the same type marker to mark the type of the function to let us change parameter and return type of methods within our class. Also, we can use the `extends` keyword to define the properties that can be referenced with our data marked by generic types. We can use the `keyof` keyword to constrain the values of the than an object can take on to the key names of another object.