DEV Community

Jemal Ahmedov
Jemal Ahmedov

Posted on

Classy way of using Typescript Generics

Overview

Within the realms of Object-Oriented Programming, generic types are a very common and useful way to build components which can work with a variety of types instead of only one. Fortunately for us, we can do the same generic types in functional programming using TypeScript Generics.

Using generic parameters

  1. Simple example

A simple generic parameter in a function looks like this:

function myFunc<T>(arg: T) {
  return value;
}
Enter fullscreen mode Exit fullscreen mode

A generic type can be defined by using <GenericAnnotation> after the name of our function. Then simply specify the type of the argument to be the generic type. The return value of a function can also be defined as a generic type, e.g. myFunc<T>(arg: any): T

  1. More Interesting Example

Let's have the following interface IUserDetails which specifies different user properties:

interface IUserDetails {
  firstName: string;
  lastName: string;
  age: number;
  dob: Date;
}
Enter fullscreen mode Exit fullscreen mode

For the sake of this example, let's imagine that each user property needs to be updated separately. One way to do this is by writing a function for each property which can be strongly typed independently for each property.

Instead of doing that, let's build a generic function which will allow to pass any of the user properties and their correct types. Here is how that might look like:

function updateUserDetails<
  TKey extends keyof IUserDetails,
  TValue extends IUserDetails[TKey]
>(key: TKey, value: TValue) {
  // Update user details
}
Enter fullscreen mode Exit fullscreen mode

Pretty cool isn't it? Let's see what the generic type constraints are doing.

There are two generics in the function, one for the key of the user property, TKey, and the other for the value of it, TValue.

  • TKey has a defined type constaint, specifying that it can only be one of the keys of IUserDetails interface.
  • TValue has a defined type constraint, specifying that the type can only be the type of the defined user property.

Calling the function like this: updateUserDetails("dob", "Jack") will throw a type error as the dob property of the interface expects a type of Date, but executing the function like updateUserDetails("firstName", "Jack") will work as it's passing the correct type forfirstName property.

Using Generic types when building React components

Typescript Generics can also be applied when building React components.

Here is an example. Let's build a List which can receive any types of list items with predefined generic constraints.

This is the bare minimum a List item object needs to have:

interface IBaseListItem {
  id: string;
  name: string;
}
Enter fullscreen mode Exit fullscreen mode

The props of the list will look like this:

interface IListProps<T extends IBaseListItem> {
  title: string;
  items: T[];
}
Enter fullscreen mode Exit fullscreen mode

The items[] property is defined as a generic type with a constaint which provides the bare minimum the type needs to have in order to be accepted, in this case is IBaseListItem.

The component definition can look like this:

function List<T extends IBaseListItem>(props: IListProps<T>) {
  return (
    <ul>
      {props.items.map((it) => (
        <li key={it.id}>{it.name}</li>
      ))}
    </ul>
  );
}
Enter fullscreen mode Exit fullscreen mode

The react component has defined generic type which is provided to the generic props IListProps. As the generic type of the function has the same generic constraint as the IListProps, the generic type will be accepted.

Top comments (1)

Collapse
 
joebobmiles profile image
Joseph R Miles

Nice! Had a brief moment where I was worried that using the generic would create a parser error, since it's natural to assume it'd look like: <List<number> />. Then I remembered that, unlike C# (or a lot of languages with generics), TypeScript infers the type parameter, which means the usage would look the same as a non-generic component. Very clever!