Generics exist in typescript, and they behave similarly to other typed languages like Java, Kotlin or C#. And they can become useful as generics do when creating function dealing with multiple types.

But that’s not just it! However, the syntax can be off-putting for some uninitiated, so we’ll look over some of the generics’ usage in typescript to warm up.

Then I have prepared some examples to illustrate how powerful they become, for refactoring, reducing duplication but also simplifying some object related computation.

🧰 Basics Syntax

1. Function

You can use the type as a variable with generics. Let’s have a function that takes a generic type T and returns the same type:

export function returnAnything<T>(arg: T): T {
  return arg;
}

The T gets inferred from the argument passed to the function, so you don’t need to explicitly specify the type. The return type doesn’t need to be generic and the same as the argument type.

returnAnything([1, 2, 3]) // returns [1, 2, 3]
returnAnything<string>('hello') // Also valid, returns 'hello'

The <T> between the name and the parentheses is the key to make a function generic.

2. Interface

The same way you can have a generic interface, which to be implemented needs a type specified. Let’s have an example:

export interface Walking<T> {
  walk: (arg: T) => T;
}

When implementing the interface, you need to add in your class a walk method that takes the type T passed to the interface as an argument and return the same type. In other words, you could have that walk use the parametrized type in either the return or argument.

If the generic T of the interface is not used by any member, then it can be removed to simplify the code.

3. Class

Since classes can also be generic, you can have a class that takes a generic type T and uses it in its members. Let’s have an example:

export class Biped<T> implements Walking<T> {
  walk(arg: T): T {
    return arg;
  }
}

The Biped class implements the Walking interface, so it needs to have a walk method that takes a type T. We could use the returnAnything in the walk method is would work since it can take any type.

Now this doesn’t come very close to any usable example, so let’s have a class that fixes the generic.

export class Human extends Biped<string> {
  private name: string = 'Human';
  talk(): string {
    return 'Hello';
  }
}

With this class, we have a Human that extends the Biped class with the type string. So the walk method will only take and return strings. Try with anything else and it won’t compile.

human.walk('10 meters') // returns '10 meters'

Now we should have another class extending Biped<T> with different types. In a real life project, it would most likely be custom-made objects.

The point of using generics is when you have a core logic that can be applied to multiple entities with common interfaces.

4. With constraints

Generics can be too generic, and you might want to restrict the type to a subset of types. You can do this by constraining the type with the extends keyword. Let’s have a couple of examples:

export function testLap<R, T extends Walking<R>>(walker: T, distance: R): R {
  return walker.walk(distance);
}

export function testLapWithNumber<T extends Walking>(walker: T, distance: number): number {
  return walker.walk(distance);
}

Let’s see what we have here:

  • With testLap: has two generics, one for the walker which needs to implement Walking<R> and R the distance’s type that can be walked.
    • Our Human could use walkLap<string, Human>(human, '10m') we need the <string, Human> otherwise it infers it to <'10m', Human>.
  • With testLapWithNumber: has only one generic, the walker that needs to implement Walking and the distance is a number.
    • Our Human could not use this method, because it can only walk strings 😅

A class that doesn’t implement the Walking interface would not be able to use any of the testLap method.

📒 Type Keywords

1. Using the keyof keyword

The keyof keyword is used to get the keys of an object as a union type. This is beneficial when dealing with objects in typescript, but we will see more about that in the advanced usage section.

As an example, even if it’s not very useful:

const keys = Object.keys(new Human()) as unknown as keyof Human;
// returns ['name']

The unknown is necessary for the casting because keys returns an array of string string[], so we use this hack for an easier cast. This will not be needed once we use it to loop through the object.

2. Using the typeof keyword

The typeof keyword is used to get the type of a variable or expression.

But it is limited, and the return values are only:

  • “string”, “number”, “bigint”, “boolean”, “symbol”, “undefined”, “object”, “function”

Which is why you may want to use instanceof when dealing with class.

typeof [1, 2, 3] === "object" // true
typeof "hello" === "string"   // true

There’s no default array or list type, so it returns object in that example.

3. Using the instanceof keyword

The instanceof keyword is used to check if an object is an instance of a class. Let’s have an example:

const human = new Human();
human instanceof Human; // true

Here we check if the human object is an instance of the Human class, which here is true. The instanceof keyword doesn’t work with interfaces or none class type. It checks only the implemented class.

4. Using the is keyword

The is keyword is used to define a type guard in TypeScript.

function isHuman(biped: any): biped is Human {
  return (biped as Human).talk !== undefined;
}

Now we could have that we have the isHuman function that checks and assert the type of the object. We can use that in a function in a strategy pattern where depending on the type of the object, we call different methods.

export function act(biped: Human | Other): string {
  if (isHuman(biped)) {
    return biped.talk();
  } else {
    return biped.yell();
  }
}

Here the act function will call the talk method if the object is a Human, and yell if it’s an object of type Other (which should have a yell method). Without the type guard, the compiler would complain.

⚙️ Advanced Usage

Let’s define a simple object for the examples:

export const fruitBasket = {
  apple: '🍎',
  banana: '🍌',
  kiwi: '🥝',
};

1. Iterate over an object

You might be interested to apply the same modification for multiple values of the object, however you can’t just map over an object. Here are some alternatives.

a. Using for...of Object.entries

To iterate over an object to retrieve the list of values, you can use the Object.entries method. To avoid compilation issues, we are using keyof to set the type of the value as T[keyof T]. Which can be read as the value from the key of the object of type T.

export function valuesFrom<T extends object>(obj: T): T[keyof T][] {
  const values: T[keyof T][] = [];
  for (const [, value] of Object.entries(obj)) {
    values.push(value);
  }
  return values;
}

We limit T to be an object, as this would not work with a primitive type like string or number. If I apply this function to the fruitBasket object, it will return ['🍎', '🍌', '🥝'].

Now you don’t need to save the values, you could directly apply the modification to the object and return it, depending on the use case.

b. Using for...in

You can also loop directly over the object’s keys directly with for...in. The tick to make it happen is to set the key’s type as keyof typeof obj, with obj being the object which is iterated over.

The keyof typeof obj returns a type that represents all possible keys of obj.

export function valuesOf<T extends object>(obj: T): (T[keyof T])[] {
  const values: (T[keyof T])[] = [];
  let key: keyof typeof obj;
  for (key in obj) {
    values.push(obj[key]);
  }
  return values;
}

This also returns all the values of an object, we could filter on the keys to loop only on a subset of the object. It gives a bit more flexibility than the other method. If I apply this function to the fruitBasket object, it will also return ['🍎', '🍌', '🥝'].

2. Interact with objects

a. Get a property

If you need to get the same property from multiple objects, and you don’t have an interface or some kind of pattern in place. Here we can return the object’s key value:

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

We restrict the type of the key K to be a key of the object T so it compiles the type correctly. Since K is a key of T, the value that is returned can be simply typed as T[K].

If you create a human, you can retrieve the value of the name key using:

getValue(new Human(), 'name')              // assuming `name` is a public property
getValue(new Human(), 'name' keyof Human)  // when is a `private` property

Not that you would want to use it, it is interesting to see how you can interact with objects and their properties.

b. Enhance an object

If you need to add properties to an object, you can use the Object.assign method. To keep the types in check, we will create a WithProperty which is a key K of type string and any value V that will be added to the object:

type WithProperty<K extends string, V> = {
  [key in K]: V;
};

Now as we’ve seen before we define the withProperty method that will take an object and return it with an extra property to it.

function withProperty<O extends object, K extends string, V>(
  obj: O, property: WithProperty<K, V>,
): O & WithProperty<K, V> {
  return Object.assign(obj, property);
}

The object of generic type O will be returned with the property property added to it. With an example, we could define:

const yolo: Human & { yolo: string } = withProperty(human, { yolo: '🦄' });

We don’t need to use the WithProperty when typing, because the { yolo: string } matches the inferred type. This can be useful to transform similar objects with common properties.

3. Dynamic type (with prefix)

I encountered this use case when working with SQL queries, on join you can prefix the table’s name to avoid conflicts. To type the output properly in typescript, you can create a dynamic type that prefixes the keys of an object.

If we have a Person interface that’s used for an entity in our database:

interface Person {
  id: number;
  name: string;
  age: string;
}

With a join, we might get a prefixed object like this { Person_id: 1, Person_name: 'John', Person_age: 30 }, but it would be tedious to create a JoinedPerson type, instead you can create a the type dynamically:

type Prefixed<T, P extends string> = {
  [K in keyof T as `${P & string}${string & K}`]: T[K];
}

The Prefixed type takes two generics, T the object type and P the prefix string value.

Then it defines the keys of the object of type T to be cast to the prefix and the key name. With P defined as _Person__, you can read it as:

// Same as `PrefixedWith<Person, 'Person_'>`
type Prefixed<T> = {
  [K in keyof T as `Person_${string & K}`]: T[K];
}

Which translates in an example as:

const prefixedJohn: PrefixedWith<Person, 'Person_'> = { Person_id: 1, Person_name: 'John', Person_age: 30 };

And it works! 🤯 It might have a niche usage, but I thought it was a nice trick to add in this article.

If you are interested in more javascript object manipulation, check the tricks in my article about javascript objects!