DEV Community

Cover image for Typescript generics - stop writing tests & avoid runtime-errors. pt2
Jakub Švehla
Jakub Švehla

Posted on • Edited on

Typescript generics - stop writing tests & avoid runtime-errors. pt2

TLDR:

This is The second chapter of the series where I show you the way how to avoid runtime-errors without writing tests. We'll use just strong Typescript inferring principles and generics.

You can copy-paste source code from examples into your IDE or online Typescript playground and play with it by yourself.

Chapters:

  1. Inferring

  2. Generics (current read)

In this chapter, we will look at more advanced type inferring and type reusing with Typescript generics.

In the previous chapter about typescript inferring we introduced

  • type inferring
  • typeof
  • &
  • as const
  • |

So if you did not read it or you don’t fully understand these concepts or Typescript syntax, check chapter 1.

Generics

Generics are crucial for our new inferring Typescript mindset. It enables us to perform real one-liner Typescript magic. With generics, we will be able to infer whatever we want.

In this chapter, we will introduce

  1. Generics + Type inferring

  2. Type checking using an extends subset

  3. Conditions inside of generics

  4. Type inference in conditional types

  5. Promise wrapper

  6. Utility types

  7. Custom generics utils

I don’t want to duplicate Typescript documentation so you should spend some time reading generics documentation for a better understanding of this series.

You can inspire yourself with useful resources like:

So let’s look at a brief overview of Typescript features which we have to know.

1. Generics + Type inferring

One of the main tools for creating reusable components is generics. We will be able to create a component that can work over a variety of data types rather than a single one.

We can combine generics with Typescript inferring. You can easily create a generic which will be used as the argument of our new function.

const unwrapKey = <T>(arg: { key: T }) => arg.key;
Enter fullscreen mode Exit fullscreen mode

Now we will just call this function and get a type based on implementation.


const unwrapKey = <T>(arg: { key: T }) => arg.key;
// ts infer value1 as string
const value1 = unwrapKey({ key: 'foo' });
// ts infer value1 as boolean
const value2 = unwrapKey({ key: true });
// ts infer value1 as true
const value3 = unwrapKey({ key: true } as const);
Enter fullscreen mode Exit fullscreen mode

Alt Text

Typescript dynamically infers arguments and returns the value of the function by extracting the data type of <T> which is passed as a generic value. The function is 100% type-safe even if the property key is type-agnostic.

Documentation: https://www.typescriptlang.org/docs/handbook/generics.html

2. Type checking using an extends subset

The typescript keyword extends works as a subset checker for incoming data types. We just define a set of possible options for the current generic.

const unwrapKey = <T extends boolean | number>(arg: { key: T }) => arg.key;
const ok = unwrapKey({ key: true });

const willNotWork = unwrapKey({
  value: 'value should be boolean or number'
});

Enter fullscreen mode Exit fullscreen mode

Alt Text

Documentation:
https://www.typescriptlang.org/docs/handbook/generics.html#generic-constraints

3. Conditions inside of generics

There is another usage of extends keyword for checking if the type matches the pattern. If it does, Typescript applies a type behind the question mark ?. If not, it uses the type behind the column :. It behaves the same way as the ternary operator in Javascript.

type Foo<T> = T extends number
  ? [number, string]
  : boolean

const a: Foo<number> = [2, '3']
const b: Foo<boolean> = true
Enter fullscreen mode Exit fullscreen mode

Alt Text

If the type of T is a number, the resulting type is a tuple if not, it‘s just boolean.

Documentation:
https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#conditional-types

This feature can be nicely used with Typescripts type-guards.
https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-guards-and-differentiating-types

4. Type inferring in conditional types

The typescript keyword infer is a more advanced feature. It can infer a type inside of the generic type condition declaration like in the example below.

type ReturnFnType<T> = T extends (...args: any[]) => infer R ? R : any;
const getUser = (name: string) => ({
  id: `${Math.random()}`,
  name,
  friends: [],
})
type GetUserFn = typeof getUser

type User = ReturnType<GetUserFn>
Enter fullscreen mode Exit fullscreen mode

Alt Text

You will read more about ReturnType generic later in this chapter.

I’ll recommend reading the documentation for type inferring in condition types (and usage ofinfer keyword)
https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#type-inference-in-conditional-types

5. Promise Wrapper

Typescript also perfectly works with Promises

There is a built-in Promise<...> generic which we will use in asynchronous operations. The Promise generic is just a wrapper that wraps your data into Promise “class”.

The Typescript has perfect Promise support for async, await syntax sugar such as:

const getData = () => {
  return Promise.resolve(3)
}

// each async function wrap result into Promise()
const main = async () => {
  // await unwrap Promise wrapper
  const result = await getData()
}
Enter fullscreen mode Exit fullscreen mode

Alt Text

6. Utility types

Typescript provides utility types to simplify common type transformations. These utilities are globally available in your project by default.

Documentation: https://www.typescriptlang.org/docs/handbook/utility-types.html

We will focus on two of them ReturnType<...> and Partial<...>.

6.1 ReturnType<...>

ReturnType is an absolutely phenomenal Typescript feature which we will see in many more examples!

The definition of this generic looks like this:

type ReturnType<T extends (...args: any) => any> =
  T extends (...args: any) => infer R
    ? R
    : any;
Enter fullscreen mode Exit fullscreen mode

As you can see, ReturnType just takes some function and obtains the type of the return value. It enables us to perform more hardcore type inferring. Let’s take a look it in this example

const getUser = (name: string) => ({
  id: Math.random(),
  name,
  isLucky: Math.random() % 2 === 0 
})
type User = ReturnType<typeof getUser>
Enter fullscreen mode Exit fullscreen mode

Alt Text

This is a great feature for our new Typescript inferring programming mental model which we presented in the previous chapter.

Another cool example of ReturnType<...> is getting some specific read-only value from an object inside of a function.

const foo = () => ({ foo: 'bar' } as const);
type FooReturnValue= ReturnType<typeof foo>
type bar = FooReturnValue['foo']
Enter fullscreen mode Exit fullscreen mode

Alt Text

6.2 Partial<…>

In this example, we will use an in keyof syntax feature. If you want to know more about that, read advanced Typescript documentation. https://www.typescriptlang.org/docs/handbook/advanced-types.html#index-types.

Generic Partial definition looks like:

/**
 * Make all properties in T optional
 */
type Partial<T> = {
  [P in keyof T]?: T[P];
};
Enter fullscreen mode Exit fullscreen mode

As you can see, it just wraps a Javascript object and set its keys to be possibly undefined. A question mark after the key name makes the key optional. You can use this generic if you want to use just apart of an object.

const user = {
  id: Math.random(),
  name: 'Foo',
  isLucky: Math.random() % 2 === 0
}

type PartialUser = Partial<typeof user>

Enter fullscreen mode Exit fullscreen mode

Alt Text

7. Custom generics utils

In this section, we are going to create helper generics.

7.1 Await

Await is a utility generic which takes Promise<...> wrapped value and remove the Promise wrapper and leaves only extracted data.

Try to imagine that you already have async Javascript function. As we know, each async function wraps the result into a Promise generic wrapper. So if we call ReturnType for an async function, we get some value wrapped into Promise<T> generic.

We are able to extract a return value out of a Promise using ReturnType<T> and Await<T>:


export type Await<T> = T extends Promise<infer R> ? R : T

// helper function to emit server delay
const delay = (time: number) => {
  return new Promise(res => {
    setTimeout(() => {
      res()
    }, time)
  })

}

const getMockUserFromServer = async () => {
  // some asynchronous business logic 
  await delay(2000)
  return {
    data: {
      user: {
        id: "12",
      }
    }
  }
}

type Response = Await<ReturnType<typeof getMockUserFromServer>>
Enter fullscreen mode Exit fullscreen mode

Alt Text

It adds another possibility of inferring more advanced hidden data types in Javascript code.

7.2 RecursivePartial

This is just enhanced Partial<...> generic which we introduce a few paragraphs ago. The declaration looks like this:

// inspiration: https://stackoverflow.com/a/51365037
type RecursivePartial<T> = {
  [P in keyof T]?:
    // check that nested value is an array
    // if yes, apply RecursivePartial to each item of it
    T[P] extends (infer U)[] ? RecursivePartial<U>[] :
    T[P] extends object ? RecursivePartial<T[P]> :
    T[P];
};
Enter fullscreen mode Exit fullscreen mode

RecursivePartial is initially inspired by this Stack-overflow question https://stackoverflow.com/a/51365037

As you see, it just recursively sets all keys of the nested object to be possibly undefined.

Combine all generics to one monstrous masterpiece

Okay, we learned a lot about Typescript generics. Now we will combine our knowledge together in the next paragraphs.

Imagine that we have an application that makes calls to a backend service. Backend returns data about a currently logged user. For better development, we use mocked responses from the server. Our target is to extract the response data type from mocked API calls (like getMeMock function in the example).

We don’t believe in the correctness of the response from the server so we make all fields optional.

Let’s define our utils generics and just apply a one-line typescript sequence of generics to infer the type of User from the mock function.

// ------------------- utils.ts ----------------------
// inspiration https://stackoverflow.com/a/57364353
type Await<T> = T extends {
  then(onfulfilled?: (value: infer U) => unknown): unknown;
} ? U : T;
// inspiration: https://stackoverflow.com/a/51365037
type RecursivePartial<T> = {
  [P in keyof T]?:
    T[P] extends (infer U)[] ? RecursivePartial<U>[] :
    T[P] extends object ? RecursivePartial<T[P]> :
    T[P];
};


// helper function to emit server delay
const delay = (time: number) => new Promise((res) => {
  setTimeout(() => {
    res();
  }, time);
});


// ----------------- configuration.ts ---------------
const USE_MOCKS = true as const;
// ----------------- userService.ts -----------------
const getMeMock = async () => {
  // some asynchronous business logic
  await delay(2000);
  return {
    data: {
      user: {
        id: '12',
        attrs: {
          name: 'user name'
        }
      }
    }
  };
};
const getMe = async () => {                     
  // TODO: call to server
  return getMeMock();
};

type GetMeResponse = Await<ReturnType<typeof getMeMock>>


type User = RecursivePartial<GetMeResponse['data']['user']>
Enter fullscreen mode Exit fullscreen mode

Alt Text

Do you see it too? We took almost pure javascript code and using our Typescript utils, we added only 2 lines of Typescript code and inferred all static data types from this Javascript implementation! We can still write Javascript code and enhance it with Typescript micro annotations. All of that with a minimal amount of effort without boring interface typing.

And at top of it, every time you want to access some sub-property of User type, your IDE will automatically add an optional chaining operator (name*?* ). Because we made all fields optional, accessing nested values can’t throw a new error.

Alt Text

If optional chaining does not work, you have to set up “strictNullChecks”: true, in your tsconfig.json

And that’s it! At this moment you’re able to infer whatever you want from your Javascript implementation and you’re able to use a type-safe interface without extra static types.

Pay attention! Don’t overuse Generics!

Everytime you manually pass a static type as a parameter into a generic your code starts to smell

— — Jakub Švehla — —

I believe that in your average code there are not big tricky functions with hard to understand data models. So please, don’t overthink your generics. Every time you create new generic think about if it’s necessary to create that kind of redundant abstraction which decreases code/type readability. So if you write a type by hand, be strict and clear. Generics are awesome especially for some general-purpose utility types (ReturnType, Await, Etc.). But be aware, generics in your custom data model could add extra unwanted complexity. So pay attention and use your brain and heart to do it well ❤️.

*Bad practice *😒

type UserTemplate<T> = { id: string, name: string } & T
type User1 = UserTemplate<{ age: number }>
type User2 = UserTemplate<{ motherName: string }>
type User = User1 | User2
Enter fullscreen mode Exit fullscreen mode

*Good practice *🎉

type UserTemplate = { id: string, name: string }
type User1 = UserTemplate & { age: number }
type User2 = UserTemplate & { motherName: string }
type User = User1 | User2
Enter fullscreen mode Exit fullscreen mode

An alternative notation for the good practice 🎉

type User = {
  id: string,
  name: string
} & (
    { age: number }
  | { motherName: string }
)
Enter fullscreen mode Exit fullscreen mode

Conclusion

In chapter one we learned the basics of Typescript and its features. We have new ideas about using static type inferring for Javascript.

In this chapter, we learned how to use generics, and when it’s appropriate to use them.

Do You want more?

If you're interested in more advanced type usage, look to my other articles.

Object.fromEntries<T>

Retype Object.fromEntries to support all kinds of tuples
https://dev.to/svehla/typescript-object-fromentries-389c

DeepMerges<T, U>

How to implement DeepMerge for static types
https://dev.to/svehla/typescript-how-to-deep-merge-170c

If you enjoyed reading the article don’t forget to like it to tell me that it makes sense to continue.

Top comments (0)