Type to generate custom UI components with AI

Type to generate UI components from text

OR

Browse thousands of MUI, Tailwind, React components that are fully customizable and responsive.

Explore Components

Type-Safe React: An Easy Guide to React TypeScript Integration

In today’s web development landscape, it’s becoming a norm to have a type safe applications for good reasons. Especially as building robust and maintainable applications are paramount. By harnessing the power of TypeScript within React, developers can enhance their projects with a layer of type safety, catching potential issues at compile-time and fostering a more predictable and efficient development process.

With this comprehensive guide, we’ll go over the reasons why having type-safety in React is important, the process of integrating TypeScript with React, and providing types to React hooks and components. This guide equips you with the skills needed to confidently embrace TypeScript, enhancing the reliability and scalability of your projects.

Prerequisite

To follow this guide a basic understanding of React and Typescript is assumed. Here is a list of prerequisites:

Introduction to React and Typescript

What is TypeScript?

TypeScript is a statically typed superset of JavaScript, that enhances application development by introducing static typing through annotations, providing features such as interfaces and generics, which can help to catch errors at compile time.

Why is Type Safety Important in React?

Type safety in React refers to the practice of using TypeScript, to catch and prevent type-related errors in React applications at compile-time rather than runtime. By explicitly defining and enforcing the types of data, props, state, and function parameters in a React application, ensure that we developers can make sure that our code adheres to specific constraints, reducing the likelihood of runtime errors caused by incorrect data types.

Type safety in React is important for several reasons as such:

  1. Early Error Detection: Type safety enables the detection of errors at compile-time rather than runtime. This means that it catches many common types of mistakes, such as passing incorrect props or using the wrong data types, early in the development process. This leads to faster feedback and reduces the likelihood of shipping code with critical errors.

  2. Code Clarity and Self-Documentation: Type annotations serve as documentation for the structure of data, making the code more self-explanatory. With explicit types for props, state, and function parameters, developers can easily understand the expected data shapes without having to dig through the implementation details.

  3. Enhanced Development Experience: Integrated development environments (IDEs) like Visual Studio Code leverage TypeScript’s static typing to provide features such as autocompletion, type checking, and inline documentation. This improves the overall development experience by reducing the likelihood of typos, providing helpful hints, and speeding up the coding process.

  4. Refactoring Confidence: When making changes to the codebase, type information allows developers to refactor with confidence. The IDE can identify where changes are needed, and developers can ensure that the entire codebase is updated consistently.

  5. Reduced Debugging Time: Type-safe code catches many type-related issues during development, reducing the need for extensive runtime debugging. This can lead to faster development cycles and more reliable code.

Additional Reasons

  1. Collaboration and Maintenance: In collaborative projects, type annotations help team members understand the structure of data and interfaces, facilitating smoother collaboration and making it easier for developers to maintain and extend the codebase over time.

  2. Improved Code Quality: Type safety encourages developers to be explicit about the types of data they are working with. This leads to more robust code that is less prone to runtime errors, improving the overall quality and reliability of the application.

  3. Integration with Third-Party Libraries: Type safety is particularly valuable when working with third-party libraries or APIs. TypeScript allows developers to create type definitions for external libraries, enabling a seamless integration and reducing the likelihood of runtime errors due to mismatches between expected and actual data structures.

In summary, type safety in React enhances the development process by catching errors early, improving code clarity, providing a better development experience, and contributing to overall code quality and maintainability.

With all of that, I hope you’re excited to start with TypeScript in React😉

Setting Up a TypeScript React Project

The very first step is to have Typescript installed on your local machine and to do that, execute the command below into your terminal.

npm install -g typescript

Since there are various means of setting up a React project with typescript because of the various build tools and frameworks out there, we’ll go through two processes which are setting up a React project with Vite and Next.js.

Typescript React app powered by Vite

With Vite as a build tool for React projects, you can make use of the command `npm create vite@latest` while following the steps below to initialize your React typescript project.

Vite React TypeScript project

Typescript React with Next.js

Running the command `npx create-next-app@latest` comes with TypeScript selected as the default value while initializing a Next.js project.

Next.js initialization

By just following the setup guides for both Vite and Next.js, you’ve got yourself a Typescript starter kit React application, but you’d still need to know how Typescripts works with React, so let’s go over some basic concepts you should know as a React Typescript developer.

Basic TypeScript Concepts for React Developers

File extension (.tsx) or (.ts)

When using React with TypeScript, every file that contains JSX needs to be in a .tsx file extension, which tells Typescript that the file contains JSX. In the case where the file doesn’t contain JSX, the file extension should be .ts. The file extensions (.tsx or .ts) allow you to write types, interfaces and other typescript related code.

Interface or Type declaration

Interface declaration defines the structure of an object. It specifies the properties and methods that an object must or may have, providing a way to enforce a consistent shape across various parts of a codebase.

Syntax

interface User {
  name: string;
  age: number;
  address: string;
}

Type declaration is a way to define a shape for a set of values. It can represent various data structures such as primitive types, objects, arrays, functions, or custom structures. Unlike interfaces, types are more versatile and can represent a broader range of constructs beyond object shapes.

Syntax

type User = {
  name: string;
  age: number;
  address: string;
}

Hooks with Typescript

useState

With the useState hook, type inference works well for all primitive data types such as boolean, string, number etc.

const [state, setState] = useState(true);
// `state` is inferred to be a boolean
// `setState` only takes booleans

Type inference is a feature Typescript has to automatically deduce or infers the data types of variables, expressions, or function return values based on the context and the values assigned or returned without you explicitly specifying the returned type.

In the example above, you don’t need to explicitly specify the state type as a boolean, typescript can infer the state type as a boolean value and the setState function should only take boolean values. In the case where you’d like to specify the returned value as a string, here’s how to do so:

const [name, setName] = useState<string>("Favourite");
// `name` is typed and inferred to be a string
// `setName` only takes a string

If you’re making use of an editor like VSCode, when you hover over the state, name and setName function, you should see the associated type.

There are times when you need the initial value in your state like the null value but the subsequent values should be in another shape, for example, a user object, here is how to achieve that using the union type.

type User = {
  name: string
  age: number
}

const [user, setUser] = useState<User | null>(null)
// `user` is typed to be null or User
// `setName` takes null or User type

// later
setUser({
   name: "Favourite",
   age: 21
})

// or
setUser(null)

useMemo

The useMemo hook memoises the result of a function, preventing unnecessary recalculations and rendering. It takes two values, which are the calculation function and a list of dependencies. So in order to have a type-safe useMemo hook, we need to type all its values appropriately.

const memoizedUserData = useMemo<User | null>(() => calculateUserData(userId), [userId])

In the code snippet, above we can see that the returned type from the useMemo function would be <User | null>, but the type for the calculateUserData function and the userId is very important. Below is an example showing you how to make the useMemo values (calculation function and a list of dependencies) type-safe.

import React, { useMemo, useState } from 'react';

interface User {
  id: number;
  name: string;
  age?: number
}

// Simulating fetching data based on the userId
// Expensive computation or fetching data
const calculateUserData = (userId: number): User | null => {
  if (userId === 1) {
    return {id: 1, age: 1, name: 'John Doe' };
  } else {
    return null;
  }
}

const App = () => {
  const [userId, setUserId] = useState<number>(1);
  const [userData, setUserData] = useState<User | null>(null);

  // Make useMemo type-safe by providing generics <User | null>
  const memoizedUserData = useMemo<User | null>(() => calculateUserData(userId), [userId]);

  const handleUserIdChange = (newUserId: number) => {
    setUserId(newUserId);
  };

  const handleClearUserData = () => {
    setUserData(null);
  };

  return (
    <div>
      <div>
        <label>User ID:</label>
        <input
          type="number"
          value={userId}
          onChange={(e) => handleUserIdChange(parseInt(e.target.value, 10))}
        />
      </div>
      <div>
        <button onClick={handleClearUserData}>Clear User Data</button>
      </div>
      <div>
        {/* TypeScript ensures that memoizedUserData is of type User | null */}
        <p>User Data: {memoizedUserData ? memoizedUserData.name : 'No data'}</p>
      </div>
    </div>
  );
};

export default App;

In the example about the type for the useMemo function was returned explicitly as <User | null> however, the return type from the hook can also be inferred from the returned type of the calculateUserData function. In this case, it would be written like this:

const memoizedUserData = useMemo(() => calculateUserData(userId), [userId]);

useCallback

The useCallback hook is a function that allows you to cache function definition between re-renders depending on the dependencies. Just like the useMemo hook, the function’s type is inferred from the returned value of the function in the first parameter, or the type can be declared explicitly to the hook.

const handleUserClick  = useCallback((clickedUser: UserData) => {
   setClickedUser(clickedUser);
}, []);

// handleUserClick type is (clickedUser: UserData) => void

The type for the variable handleUserClick would be inferred as (clickedUser: UserData) => void

However, you could also explicitly specify the returned type of the useCallback hook, like this:

// Make useCallback type-safe by providing generics
const memoizedCallback = useCallback<() => void>(() => {
  onUserClick(user);
}, [onUserClick, user]);

Notice the use of <() => void> in the hook useCallback<() => void>(() => {…}, […]).

useReducer

The useReducer hook takes in two values, the initial state and the reducer function. In other to make the hook type-safe, we’d need to annotate types to both the initial state and the reducer function.

Here’s how the hook would be initialised:

const [state, dispatch] = useReducer(stateReducer, initialState);

Let’s see how to type the initial state and reducer function.

import { useReducer } from "react";

interface State {
   count: number 
};

type ActionType =
  | { type: "increment"; payload: number }
  | { type: "decrement"; payload: number};

const initialState: State = { count: 0 };

function reducer(state: State , action: ActionType ): State {
  switch (action.type) {
    case "increment":
      return { count: state.count + action.payload };
    case "decrement":
      return { count: state.count - action.payload };
    default:
      throw new Error("Unknown action type");
  }
}

function App() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({ type: "decrement", payload: 5 })}>
        -
      </button>
      <button onClick={() => dispatch({ type: "increment", payload: 5 })}>
        +
      </button>
    </>
  );
}

export default App

Here’s the breakdown of the code above

  • interface State describes the shape of the reducer’s state.

  • type ActionType describes the different actions which can be dispatched to the reducer.

  • const initialState: State provides a type for the initial state, and also the type which is used by useReducer by default.

  • reducer(state: State , action: ActionType ): State sets the types for the reducer function’s arguments and return value as State.

useRef

The useRef hook

To make the useRef hook type-safe in React with TypeScript, you can use generics to specify the type of the referenced element. This ensures that the current property of the RefObject is appropriately typed.

Here’s an example:

import React, { useRef, useEffect } from 'react';

interface MyComponentProps {
  text: string;
}

const MyComponent: React.FC<MyComponentProps> = ({ text }) => {
  // Define the type of the referenced element using generics
  const myRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    // Access the current property with type safety
    if (myRef.current) {
      console.log(myRef.current.textContent);
    }
  }, [text]);

  return <div ref={myRef}>{text}</div>;
};

export default MyComponent;

In this example:

  • The useRef hook is used with the generic type parameter <HTMLDivElement> to indicate that the current property will reference a div element.

  • The myRef object is created with the appropriate type of information.

  • Inside the useEffect hook, TypeScript ensures type safety when accessing properties of the referenced element (myRef.current), preventing potential runtime errors.

By using generics with useRef, you enhance type safety and avoid common pitfalls when working with React’s ref functionality.

useContext

The useContext hook is used to pass data down the component tree without passing data as component props.

We can make the hook type safe, at the point of creating the context, the parameter value type of the createContext function is then inferred as the type of the context. In the example below, the parameter value to the createContext function is “system” which is a string. The type argument for the createContext function is then specified as the type Theme where we have specific strings such as “light” or “dark” or “system”.

import { createContext, useContext, useState } from 'react';

type Theme = "light" | "dark" | "system";
const ThemeContext = createContext<Theme>("system");

const useGetTheme = () => useContext(ThemeContext);

export default function MyApp() {
  const [theme, setTheme] = useState<Theme>('light');

  return (
    <ThemeContext.Provider value={theme}>
      <MyComponent />
    </ThemeContext.Provider>
  )
}

function MyComponent() {
  const theme = useGetTheme();

  return (
    <div>
      <p>Current theme: {theme}</p>
    </div>
  )
}

The images below show the type useContext hook and other context provider related values.

React Components with TypeScript

Now that we’ve gone over how to make most React hooks type safe with TypeScript, let dive into how to make React components type safe.

When a react component is specifed, it’s infered type is JSX.Element however the type can be explicitly specified, just like the example below:

// App.tsx file

// Inferred returned type: JSX.Element
const App = () => {
  return (
    <div>...</div>
  )
}

// Inferred returned type: JSX.Element
function App() {
  return (
    <div>...</div>
  )
}

// Explicitly specified returned type JSX.Element
const App = (): JSX.Element => {
  return (
    <div>...</div>
  )
}

const App: React.FunctionComponent = () => (
  <div>...</div>
);

How to type props in a React component

Props object types can either be a type or interface definition and then specified as the typed props like the examples below:

// Declaring type of props - see "Typing Component Props" for more examples
type AppProps = {
  message: string;
}; 

// Easiest way to declare a Function Component; return type is inferred.
const App = ({ message }: AppProps) => <div>{message}</div>;

const App = ({ message }: AppProps): React.JSX.Element => <div>{message}</div>;

// Inline the type declaration
const App = ({ message }: { message: string }) => <div>{message}</div>;

// Use `React.FunctionComponent` (or `React.FC`) generic.
const App: React.FunctionComponent<{ message: string }> = ({ message }) => (
  <div>{message}</div>
);

Common Pitfalls and Best Practices

[Embed table here: https://www.notion.so/react-typescript-e5d7c892367c4679b453d0cdfe6a75b8?pvs=4#f0225af2a3b249f1ad9bd8576cbad5c5]

Real Life Example Application with Typescript

Just like every starter kit project with a new language, we’ll build a todo app, which showcases Typescript with react components in action.

So the basic idea for a Todo list app, is that a user would be able to submit a todo, mark it as done, and then delete the todo. Let’s build this application in a type safe way, powered with Typescript. Here’s a demo of the application we would be building.

How’s how we start by creating the todo app.

// TodoApp.tsx
import React, { useState } from 'react';
import TodoItem from './TodoItem';

interface Todo {
  id: number;
  text: string;
  completed: boolean;
}

const TodoApp: React.FC = () => {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [newTodo, setNewTodo] = useState<string>('');

  const addTodo = () => {
    if (newTodo.trim() !== '') {
      setTodos((prevTodos) => [
        ...prevTodos,
        { id: Date.now(), text: newTodo, completed: false },
      ]);
      setNewTodo('');
    }
  };

  const toggleTodo = (id: number) => {
    setTodos((prevTodos) =>
      prevTodos.map((todo) =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );
  };

  const deleteTodo = (id: number) => {
    setTodos((prevTodos) => prevTodos.filter((todo) => todo.id !== id));
  };

  return (
    <div>
      <h1>Todo App</h1>
      <div>
        <input
          type="text"
          value={newTodo}
          onChange={(e) => setNewTodo(e.target.value)}
          placeholder="Add a new todo"
        />
        <button onClick={addTodo}>Add</button>
      </div>
      <ul>
        {todos.map((todo) => (
          <TodoItem
            key={todo.id}
            todo={todo}
            onToggle={toggleTodo}
            onDelete={deleteTodo}
          />
        ))}
      </ul>
    </div>
  );
};

export default TodoApp;

Explanation:

  • Todo is an interface defining the structure of a todo item.

  • todos state is an array of Todo items managed by the useState hook.

  • The toggleTodo and deleteTodo functions accept an id parameter of type number.

  • The TodoApp component maps over todos, rendering a TodoItem for each todo, passing appropriate props.

Here’s how to type the TodoItem component:

// TodoItem.tsx
import React from 'react';

interface TodoItemProps {
  todo: {
    id: number;
    text: string;
    completed: boolean;
  };
  onToggle: (id: number) => void;
  onDelete: (id: number) => void;
}

const TodoItem: React.FC<TodoItemProps> = ({ todo, onToggle, onDelete }) => {
  return (
    <li>
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={() => onToggle(todo.id)}
      />
      <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
        {todo.text}
      </span>
      <button onClick={() => onDelete(todo.id)}>Delete</button>
    </li>
  );
};

export default TodoItem;

Explanation:

  • TodoItemProps is an interface that defines the expected props for the TodoItem component.

  • onToggle and onDelete are callback functions passed as props, specifying that they take an id parameter of type number and return void.

  • The component receives TodoItemProps as a generic type for React.FC, ensuring correct prop types.

So you see by incorporating TypeScript, the code benefits from static typing, preventing common errors, for example, everyone can see that the onToggle function takes a number as it parameter and provides a clearer understanding of the application’s structure. Interfaces and generics play a crucial role in defining and enforcing the types used throughout the components.

When thinking about building a type safe application, you should also be thinking of building great UI for your application. This is wherePureCode AI comes in and saves the day. PureCode AI consists of over 1000 custom components ranging from Sidebar, Sign in, Card and many more components. Try out PureCode AI today.

Frequently Asked Questions (FAQs)

In this section, we’ll address some of the commonly asked questions and misconceptions about Typescript with React.

Should you learn TypeScript for React?

Yes, learning TypeScript for React is recommended as it enhances code quality, provides better tooling support, and helps catch errors early in the development process.

Which is better TSX or JSX?

TSX is generally preferred when using TypeScript with React, as it allows for type annotations and improved type safety. JSX can be used when you don’t need type annotations and you aren’t in a typescript project.

Can you use TypeScript and JavaScript together in React?

Yes, it’s possible to gradually introduce TypeScript into a JavaScript-based React project, allowing for a smooth transition.

Should I start React with TypeScript?

It depends on your preference and experience, but starting with TypeScript in React can offer advantages in terms of code quality and maintenance.

Does ReactJS use TypeScript?

While the core of React is written in JavaScript, many React projects, including those using ReactJS, leverage TypeScript for additional type safety.

Is TypeScript better than JavaScript for React?

It depends on project requirements, but TypeScript’s static typing and tooling support can be advantageous for larger codebases.

Conclusion

In conclusion, leveraging TypeScript with React in the development of a simple Todo application enhances the robustness, maintainability, and overall developer experience. By employing TypeScript features such as interfaces, generics, and static typing, we achieve:

  1. Type Safety: TypeScript ensures that our components adhere to specific prop and state structures, catching potential errors during development rather than at runtime.

  2. Code Organization: Interfaces provide clear contracts for props and state, improving code readability and making it easier for developers to understand and collaborate on the codebase.

  3. Component Reusability: Generics allow for the creation of reusable components, promoting a modular and scalable architecture.

  4. Improved Development Workflow: Static typing and TypeScript tooling enhance the development workflow by providing intelligent autocompletion, type checking, and better documentation.

By embracing TypeScript in React, developers can build more reliable and maintainable applications, particularly as projects grow in complexity. The provided Todo application serves as a practical example of these principles in action, demonstrating how TypeScript can be seamlessly integrated into a React project for a more robust and enjoyable development experience.

Favourite Jome

Favourite Jome