Understanding Generics in Typescript

shashwatnautiyal

Shashwat Nautiyal

Posted on January 1, 2023

Understanding Generics in Typescript

Learn how generics in TypeScript can save your time in development

What are generics in TypeScript?

Generics in typescript give you the advantage of writing functions or classes where that single function or class can operate with any data type like an array, string, number, or object.

Ex- You want to create a stack data structure that can be used with any data type or write a custom hook for API services that returns typed data instead of “any” type.

Let us understand with code 🧑🏻‍💻

class Stack<StackType> {
    private items: StackType[];
    private top: number;
    constructor() {
        this.items = [];
        this.top = -1;
    }

    push(element: StackType) {
        this.items[++this.top] = element
    }

    pop() {
        return this.items[this.top--];
    }

    peek(){
        return this.items[this.top];   
    } 

    isEmpty(){
       return this.top === 0;   
    }

    size(){
        return this.top + 1;
    }
}
let stack = new Stack<string>();
Enter fullscreen mode Exit fullscreen mode

Explanation 🙇🏻‍♂️

Here we have created a generic stack class that accepts the type argument in angle brackets “<>”. This stack class will create an items array of type parameter StackType and the push function will only accept the StackType in a parameter.

Ex- If we create a number Stack by let stack = new Stack<number>() then stack.push("hello world") will throw an error. You can only pass numbers in the push function like stack.push(1).

Diving deep into generics

There are mainly 3 main types of generics

  1. Generic classes
  2. Generic functions
  3. Generic constraints

Generic classes are mainly used to create a generic data structure that can have multiple methods and variables. The type argument is passed only once in the generic class when it is instantiated.

class Todo<TodoType> {
  todos: TodoType[];
  constructor() {
    this.todos = [];
  }
addTodo(todo: TodoType) {
    this.todos.push(todo);
  }
getTodos() {
    return this.todos;
  }
}
Enter fullscreen mode Exit fullscreen mode

Generic functions are functions that can accept any data type and return data accordingly. This makes the function more reusable and flexible.

let todos: any[] = [];
function addTodo<TodoType>(todo: TodoType) {
  todos.push(todo);
}
Enter fullscreen mode Exit fullscreen mode

Generic constraints are very similar to generic types but it uses the extend keyword to force the user to add required parameters in a function call. The generic constraints add the constraint in the generic type function or class.

function removeKey<Type, Key extends keyof Type>(obj: Type, key: Key) {
  const _obj = {...obj}
  delete _obj[key];
  return _obj;
}
function updateTodo<TodoType extends {
  id: number
}>(newTodo: TodoType) {
  todos.forEach((todo, index) => {
    if(todo.id === newTodo.id) {
      todos[index] = {todo: removeKey(newTodo, "id"), id: newTodo.id}
    }
  })
}
Enter fullscreen mode Exit fullscreen mode

In the above example, the “extends” keyword ensures that the id property exists in the newTodo and we can use the id in the function to find the todo and update it accordingly.

Code sandbox

How generics can help you in React?

You can take advantage of generics in React to create generic functions, custom hooks, or generic components. I will give an example of each which can help you to write your generics.

Use-cases 👀

Generic custom hook for fetching API

export const useFetch = <DataT>(api: string) => {

  const [status, setStatus] = useState<
    "fetching" | "success" | "error"
  >("fetching")
  const [data, setData] = useState<DataT>();
  const [error, setError] = useState<string>();
useEffect(() => {
    const fetchApi = async () => {
      try {
        setStatus("fetching")
        const res = await fetch(api);
        const data = await res.json();
        setData(data);
        setStatus("success")
      } catch(err) {
        setError(err)
        setStatus("error")
      }
    }
    fetchApi();
  }, [api])

  return {data, status, error} as const;
}
const { data, status, error } = useFetch<{
  title: string,
  description: string,
  id: number
}>(API);
Enter fullscreen mode Exit fullscreen mode

Generic function for creating an array of mixed data type

function createArray<Type>(
  ...values: Type[]
): Type[] {
  return [...values]
}
const myArray = createArray<number | string>(
  "Hello", "World", 2022
);
Enter fullscreen mode Exit fullscreen mode

Generic component that renders a list without using map

const List = <T extends unknown>({
  data,
  keyExtractor,
  renderItem
}: {
  data: T[];
  keyExtractor: (item: T) => string | number;
  renderItem: (item: T) => React.ReactNode;
}) => {
  return (
    <>
      {data.map((item) => (
        <React.Fragment key={keyExtractor(item)}>
          {renderItem(item)}
        </React.Fragment>
      ))}
    </>
  );
};
<List
  data={data}
  keyExtractor={({ id }) => id}
  renderItem={(item) => <div>{item.title}</div>}
/>
Enter fullscreen mode Exit fullscreen mode

Code sandbox

Conclusion

In this article, we saw how generics can help you to write more reusable functions, components, and custom hooks. We also saw the different use cases of generics that helps you to build your logic with generics in React. It also provides a very good experience with IntelliSense integration in the IDE.

💖 💪 🙅 🚩
shashwatnautiyal
Shashwat Nautiyal

Posted on January 1, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related