DEV Community

Cover image for Generics in Typescript – How To Type Reusable Parts of Code?
Mariusz for Pagepro

Posted on • Edited on • Originally published at pagepro.co

Generics in Typescript – How To Type Reusable Parts of Code?

If you follow the DRY rule in writing your code in Typescript, the Generic Types are for you!

Thanks to Generic Types you can easily create reusable functions, interfaces or classes instead of writing single types for each one.

Generics allow you to use different types of providing parameters to one reusable part of code.

The <> syntax is reserved for describing generic type. Between the <> you can provide type which can also take default or static values.

interface User<T=string> {
  nick: T
}

const user1:User = { nick: 'User1'}
const user2:User<'User2'> = { nick: "User2"}

Let’s get some practice!

We would like to create a simple React App with TS which displays 3 titles of fetched posts with author name, loader if data is fetching and the error if we couldn't get data somehow.

To understand how it exactly works, let’s start with creating two custom hooks - one for fetching user data and the second one to get post data.

// First hook

interface User {
  id: number;
  username: string;
  email: string;
  name: string;
}

// Interface which describes data returned by useUsersFetch hook 
interface ReturnedUserData {
 data?: User[];
 error?: string;
 loading: boolean;
}

export const useUsersFetch = (): ReturnedUserData => {
 const [fetchedData, setFetchedData] = React.useState<
   ReturnedUserData>({
     loading: true
 });

 React.useEffect(() => {
   const fetchData = async (): Promise<void> => {
     try {
       const { data } = await axios.get(`${base}/users`);

       setFetchedData({ data, loading: false, error: undefined });
     } catch {
       setFetchedData({
         data: undefined,
         loading: false,
         error: "Sth went wrong."
       });
     }
   };

   fetchData();
 }, []);

 return fetchedData;
};

// Second hook

interface Post {
 id: number;
 title: string;
 body: string;
 userId: number;
}

// Interface which describes data returned by usePostsFetch hook 
interface ReturnedPostsData {
 data?: Post[];
 error?: string;
 loading: boolean;
}

export const usePostsFetch = (): ReturnedPostsData => {
 const [fetchedData, setFetchedData] = React.useState<ReturnedPostsData>({
   loading: true
 });

 React.useEffect(() => {
   const fetchData = async (): Promise<void> => {
     try {
       const { data } = await axios.get(`${base}/posts`);

       setFetchedData({ data, loading: false, error: undefined });
     } catch {
       setFetchedData({
         data: undefined,
         loading: false,
         error: "Sth went wrong."
       });
     }
   };

   fetchData();
 }, []);

 return fetchedData;
};

We have 2 separate hooks to get users and posts data.

It is not written with DRY rule, so how we can optimize our code to be more reusable if we want to make more requests in our app?

Let’s check the difference between following hooks and move them to arguments of our new reusable hook.

First of all, let's analyze the differences.

As we can see there are a few dissimilar things like url to the endpoint, returned type of data, and hook state type. So what we can do with it?

We need to create a custom hook that we will be calling useFetch with reusable logic. This hook will have one argument which is url.

But how we can pass specific type of returned data to our logic?

Well, our superheroes are GENERICS TYPES.

Let’s do it.

/* ReturnedData<T> is an interface which describes data returned by 
hook. Here we use previous interface body but we need to add generics 
type. Thanks to that it is more reusable and data can be any of type
passed as T.*/
interface ReturnedData<T> {
  data?: T;
  error?: string;
  loading: boolean;
}

// FetchedData is a type passed to useFetch during calling a hook.
export const useFetch = <FetchedData>(
  url: string
// ReturnedData<FetchedData> - We pass here data type to our generic
// interface.
): ReturnedData<FetchedData> => {
  const [fetchedData, setFetchedData] = React.useState<
    ReturnedData<FetchedData>
  >({
    loading: true
  });

  React.useEffect(() => {
    const fetchData = async (): Promise<void> => {
      try {
        // Static url replaced by dynamic param passed to hook 
        const { data } = await axios.get(`${base}${url}`);

        setFetchedData({ data, loading: false, error: undefined });
      } catch {
        setFetchedData({
          data: undefined,
          loading: false,
          error: "Sth went wrong."
        });
      }
    };

    fetchData();
  }, []);

  return fetchedData;
};

During invoking the hook we should pass a returned data type between <> syntax like <Users[]>. 

const users = useFetch<User[]>("/users").
const posts = useFetch<Post[]>("/posts").

Finally, our code is clarified, reusable and well-typed.

You can review the created code on CodeSandbox.

Link here:
https://codesandbox.io/s/dreamy-cloud-oocxq?eslint=1&fontsize=14&hidenavigation=1&theme=dark

Summary

To sum up, using Generics in Typescript gives you the ability to pass a huge range of types to a component and add an additional abstraction layer to your code which makes it being written with DRY rule.

Remember that we can apply generics to functions, interfaces and classes in Typescript.

I hope this example helped you to understand what generics are, how we can use them and why.

Top comments (0)