Typescript benefits without... using Typescript!

Typescript benefits without... using Typescript!

Featured on Hashnode

Many developers wonder, whether they should use Javascript or Typescript for their next projects, or perhaps migrate their current projects to Typescript. What I will try to show in this post though is, that this is not necessarily black and white decision, often you can use Typescript partially, often you already do thanks to editors like Visual Studio Code, as most of external libraries ship with TS types, so you already get things like hints and autocomplete, thanks to... Typescript!

Main reason to switch to Typescript

The bigger application is, the more important is to know types for our variables, functions, classes and so on, as well as to have a guarantee that we use those properly. This is exactly what Typescript gives us in the contrast to pure Javascript. What's interesting though is that you can have part of your app written in Javascript and the rest in Typescript, this is no either or decision, let's see how!

useQuery type

Look at the top picture of this post, notice that we can see data type despite the fact this file is written in Javascript! In order to see how this could be achieved, let's write a type for useQuery function. Don't worry what it does exactly, this won't be needed to understand this post. If you are curious though, useQuery is a React hook which gets a server response from a Redux store. Anyway, going back to writing useQuery type:

interface Response {
  data: any;
  loading: boolean;
  error: any;
}

function useQuery(props: { type: any }): Response;

Think about it as a prototype for now, this is by no means finished. First of all, type is defined as any, we will fix it soon. But there is more important thing, data is also of type any! But we really cannot define it as a specific type, because useQuery is a reusable function! What should we do then? Typescript generics to the rescue!

Data Generic

What are generics? You could think about them as variables for types! Let's add it to useQuery:

interface Response<Data> {
  data: Data;
  loading: boolean;
  error: any;
}

function useQuery<Data>(props: { type: any }): Response<Data>;

Now, we could use it like:

interface User {
  id: string;
  username: string;
}

const {
  data,
  loading,
  error,
} = useQuery<User>({ type: fetchUser });

Ok, but this is different than advertised at the beginning! First of all, we provide User interface to useQuery. Secondly, you can pass generics only in Typescript files! Before we fix that, let's solve type: any in useQuery. What is fetchUser? This is nothing else than Redux action creator! Actually this is a specific Redux action creator, which creates so called RequestAction from redux-requests library. Let's use this information to improve useQuery type:

import { RequestAction } from '@redux-requests/core';

interface Response<Data> {
  data: Data;
  loading: boolean;
  error: any;
}

function useQuery<Data>(props: { type: () => RequestAction }): Response<Data>;

How does it help us with Data generic though? It turns out that RequestAction also has an optional Data generic. This is hard to explain verbally, but Typescript can intelligently deduct that passed generics could be connected, which is related to type inference concept!

Generics type inference

So what we want to achieve is to have data typed without passing Data generic to useQuery. For a start, we need to make Data generic optional then:

import { RequestAction } from '@redux-requests/core';

interface Response<Data> {
  data: Data;
  loading: boolean;
  error: any;
}

function useQuery<Data = any>(props: { type: () => RequestAction }): Response<Data>;

We did it by appending = any to Data generic. Now, let's pass Data generic to RequestAction:

import { RequestAction } from '@redux-requests/core';

interface Response<Data> {
  data: Data;
  loading: boolean;
  error: any;
}

function useQuery<Data = any>(props: {
  type: () => RequestAction<Data>;
}): Response<Data>;

This is where the magic happens! The key here is that useQuery and RequestAction use the very same generic! Now, if a generic is passed to type function, then useQuery will pick it automatically! Let's see this in practice:

import { RequestAction } from '@redux-requests/core';

interface User {
  id: string;
  username: string;
}

export function fetchUser(): RequestAction<User> {
  return { type: 'FETCH_USER' };
}

We don't need to think about fetchUser implementation, all that matters is that it has User generic passed. Now, useQuery could look like that:

import { useQuery } from '@redux-requests/react';

import { fetchUser } from './actions';

const { data } = useQuery({ type: fetchUser });

That's it! This could be even Javascript file and data would have User type anyway! You don't need to pass Data generic to useQuery anymore, because it is automatically taken from fetchUser.

Of course, fetchUser has to be written in Typescript, so you might ask why we would do that. One of the reasons might be that useQuery to get user object could be used in multiple places, while fetchUser had to be declared only once. All of those places would have proper types automatically. Another benefit is that you could have those types even in Javascript files!

All in all, it depends on the use case, but this pattern of reusing generics is definitely worth knowing about. If you are interested in more possible use cases, I recommend you to check Typescript guide of redux-requests library. It takes this concept even further, for example you also get automatic type inference wherever you dispatch request actions! Happy JS and TS mixing!