The real power of the generics in Typescript

May 10, 2020

5 min read

In Typescript, generics are often present in our code. However, at least in my case, it’s because I consume them instead of declare them, but there’s much more to do with it. Based on a real example, I would like to explain you why generics can be so powerful ⚔.

Generics: A major part of software engineering is building components that not only have well-defined and consistent APIs, but are also reusable. Components that are capable of working on the data of today as well as the data of tomorrow will give you the most flexible capabilities for building up large software systems. typescript offical repo

The theory from the official typescript page is clear, but what does that mean when it’s in practice? Let’s check a real example.

Google Analytics real case

Most of the professional environments, they are using analytics. They could have their own solution, or re-use a general solution as Google analytics or Adobe analytics, etc. In my current job, we are using Google Analytics. Our goal, mainly, is to track views and clicks on our components. Every component has it’s own format to track in the body, so it needs to be different for everyone. It could easily applied that everyone has its own type definition explaining what do you expect at the moment that you create the object of tracking.

However, it was intended to create a general structure for them, a ‘contract’ specifying that always that a components wants to track, needs to fulfill some general spots that are shared between all components. Here’s the picture:

export const getEvent = ({ event, category, action, label, body }) => ({
  area: MY_AREA, // In which page you are
  event, // A click or a view event
  scope: "ecommerce",
  currencyCode: "EUR",
  eventInfo: {
    category: `Enhanced Ecommerce | ${category}`,
    action, // Again, a click or a view
    label,
  },
  ...body,
})

 

Mainly this object generated is shared between all components. Having this component, we can ensure that all tracking will always take care to have a proper format to be sent into analytics, so no need to repeate the same structure for every component, and more over open the risk of more bugs.

This is how it looks my type for that:

type TrackEvent = {
  event: string
  category: string
  action: 'Click' | 'View' | 'Slide'
  label: string
  body: ??????
}

 

How can I know the type of body? This is the part that will be re-used for every component, but on the same time it’s always different. So here is the power of generics

type TrackEvent<T> = {
  event: string
  category: string
  action: "Click" | "View" | "Slide"
  label: string
  body: T
}

 

From now on, TrackEvent accepts one parameter, and this parameter is passed to be in the body. Examples:

const myEvent: TrackEvent<string> = { body: "a string for the generic" }
const myEvent: TrackEvent<number> = { body: 22222 }
const myEvent: TrackEvent<string[]> = { body: ["hey", "ho"] }

 

Obviously these examples would complain cause you are missing the other types(event, category, etc..), but I just wanted to show how is used without any other context, so it’s simple and straightforward.

From now, I am missing one piece of the puzzle: This type is used in a function, not in a new variable:

export const getEvent = <T>({ event, category, action, label, body }: TrackEvent<T>) => ({
  area: GENERATOR_META,
  event,
  scope: 'ecommerce',
  currencyCode: 'EUR',
  eventInfo: {
    category: `Enhanced Ecommerce | ${category}`,
    action,
    label,
    variableUsage1: undefined,
    variableUsage2: undefined,
  },
  ...body,
})
// Or if you prefer
export function getEvent <T>({ event, category, action, label, body }: TrackEvent<T>) {

 

What is going on here? From now on, the function is expecting one Generic( The first T after the variable declaration), and in the TrackEvent we pass this generic. Typescript is really clever, and know that this generic is associated into the body, so it will infer automatically for you.

let body = "hello"
getEvent({ event, category, action, label, body }) // Will automatically infer as String

body = {
  track: "hello",
}
getEvent({ event, category, action, label, body }) // Will automatically infer as { track: string}

// You Can force as well the type
getEvent<string>({ event, category, action, label, body }) // This enforce the body to be a string

As you can see, we found a way to declare the type of body, even not kowing what is its structure. In my opinion, Typescript does really good its job: automatic infer types. So I believe in it and I don’t usually force the type when I invoke the function. However, if you need an extra layer of restriction, or a huge amount of people are often working in that file, it could help. But as I previously said, I support the idea of not explicitly saying which type do you need when the functions is invoked.

I hope this post helps you to understand a bit more about generics, and that we can do more than just consume them. Go ahead, and use the power of TS ⚔️⚔️⚔️⚔️🔥🔥🔥🔥


© 2020, Built with ❤️