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!
Top comments (1)
hi