Codementor Events

Typescript Generics

Published Dec 18, 2022
Typescript Generics

Hello Coding Wizards! I want to write briefly on using Typescript and specifically using generics when creating multipurpose functions. The more I play around with them, the more powerful I realise they are, so I'd like to share the power with all of you.

So What Is?

A Generic allows you to pass types into a function (or even a React Component) that will allow you to ensure within the function there is correct typing according to the schema you are working with. An example will express better than words.

Here's a basic javascript function that will accept an array of values and return an array of type {value:any, label:string}, a best-practice array of objects when giving values to a dropdown list. The function will also sort the array, so I will call it formatAndSortList:

const formatAndSortList = (list, valueKey, labelKey) => {
  return list
    .map((item) => {
      const value =
        typeof valueKey === "function" ? valueKey(item) : item[valueKey];
      const label =
        typeof labelKey === "function" ? labelKey(item) : item[labelKey];
      return { value, label };
    })
    .sort((a, b) => (a.label.toLowerCase() < b.label.toLowerCase() ? -1 : 1));
};

The valueKey and labelKey can also be a function that allows the user to return whatever he/she wants from the individual object. Let's typescript-ify this:

interface User {
  userId: number;
  firstName: string;
  lastName: string;
}

interface Option {
  value: any;
  label: string;
}

const formatAndSortList = (
  list: any[],
  valueKey: any | ((item: any) => any),
  labelKey: string | ((item: any) => string)
): Option[] => {
  return list
    .map((item) => {
      const value =
        typeof valueKey === "function" ? valueKey(item) : item[valueKey];
      const label =
        typeof labelKey === "function"
          ? labelKey(item)
          : (item[labelKey] as string);
      return { value, label };
    })
    .sort((a, b) => (a.label.toLowerCase() < b.label.toLowerCase() ? -1 : 1));
};

const users: User[] = [
  { userId: 1, firstName: "Sean", lastName: "Hurwitz" },
  { userId: 2, firstName: "James", lastName: "Baxter" },
  { userId: 3, firstName: "Melody", lastName: "Brain" },
  { userId: 4, firstName: "Nelson", lastName: "Mandela" },
];

const formattedUsers = formatAndSortList(
  users,
  "userId",
  (user) => `${user.firstName} ${user.lastName}`
);

console.log("formattedUsers", formattedUsers);

/*
formattedUsers [
  { value: 2, label: 'James Baxter' },
  { value: 3, label: 'Melody Brain' },
  { value: 4, label: 'Nelson Mandela' },
  { value: 1, label: 'Sean Hurwitz' }
]
*/

It works, great, but what if I tried this:

const formattedUsers = formatAndSortList(
  users,
  "userId",
  (user) => `${user.names.firstName} ${user.names.lastName}`
);

I'd get a runtime error because the names object does not exist. So the typing isn't so perfect. How do I let the function know exactly what kind of array I'm using?

Generics to the Rescue!

Here is the modified function:

const formatAndSortList = <Type extends { [x: string]: any }>(
  list: Type[],
  valueKey: keyof Type | ((item: Type) => any),
  labelKey: keyof Type | ((item: Type) => string)
): Option[] => {
  return list
    .map((item) => {
      const value =
        typeof valueKey === "function" ? valueKey(item) : item[valueKey];
      const label =
        typeof labelKey === "function"
          ? labelKey(item)
          : (item[labelKey] as string);
      return { value, label };
    })
    .sort((a, b) => (a.label.toLowerCase() < b.label.toLowerCase() ? -1 : 1));
};

We a declaring a <Type> generic before our argument list in parentheses, and then we are able to type our argument list using it. Note: The variable Type can be named whatever you want

Now you have awesome intellisense (in VSCode at least) for your function:

Screenshot 2022-12-18 at 07.26.52.png

Screenshot 2022-12-18 at 07.27.35.png

because Typescript knows, since you passed through an array of type User for the first argument, that Type renders as User and can be identified in the next two arguments on the fly. Like magic!

Such that if you try the nasty trick above:

Screenshot 2022-12-18 at 07.29.40.png
You will get a compiler error instead, saving you many frustrating whitescreens in your projects!

Generics also work when describing interfaces. For example:

interface User {
  userId: number;
  firstName: string;
  lastName: string;
}

interface Project {
  id: string;
  name: string;
}

interface Pagination {
  take: number;
  skip: number;
  total: number;
}

interface UsersPaged {
  pagination: Pagination;
  data: User[];
}

interface ProjectsPaged {
  pagination: Pagination;
  data: Project[];
}

I'd want to have a pagination payload for every schema in my project ideally. It's cumbersome to create a {Schema}Paged interface each time. Instead, I can have a generic schema:

interface PaginationSchema<Schema> {
  pagination: Pagination;
  data: Schema[];
}

And invoke it as such:

const usersPaged: PaginationSchema<User> = {
  pagination: { take: 1, skip: 0, total: 1 },
  data: [{ userId: 3, firstName: "s", lastName: "h" }],
};

Conclusion

Sometimes I just sit back in awe at the power at my fingertips. It's just what the joy of coding is all about. I hope you enjoyed this introduction to generics, and hope you try it out in your next project! Let me know what you think in the comments below.

Til next time, keep coding!

~Sean

Discover and read more posts from Sean Hurwitz
get started