Using Typescript with Next.js: The Only Guide You Will Need

Share with a friend:

1. Introduction to Next.js and TypeScript

What is Next.js?

Next.js is a popular open-source React framework that allows developers to build server-side rendered (SSR) and statically generated web applications with ease. It provides powerful features like code splitting, automatic static optimization, server-side rendering, and more. Next.js follows the “Zero Configuration” principle, making it simple to get started while allowing for customization when needed. Are you new to Next.js? Visit here to learn more.

What is TypeScript?

TypeScript is a typed superset of JavaScript that adds static type-checking to your code. It enables you to catch type-related errors during development, making your code more reliable and maintainable. TypeScript introduces interfaces, enums, generics, and other advanced features that help you build robust applications with confidence. Visit here for a comprehensive guide on Typescript.

2. Setting Up a Next.js Project with TypeScript

Before we dive into building applications, let’s set up a new Next.js project with TypeScript from scratch. Ensure you have Node.js and npm (or yarn) installed on your system. Let’s start:

Step 1: Create a New Next.js Project

To create a new Next.js project, we can use the following command:

npx create-next-app@latest my-nextjs-app --typescript
cd my-nextjs-app

This will scaffold a new Next.js project with TypeScript support in a directory called “my-nextjs-app.”

Step 2: Project Structure Overview

The project structure for a Next.js app with TypeScript is quite similar to a standard Next.js project, but with TypeScript files (.ts and .tsx) instead of JavaScript files (.js and .jsx). Here’s an overview of the key directories and files:

my-nextjs-app/
|-- pages/
|   |-- index.tsx
|   |-- about.tsx
|-- components/
|   |-- Header.tsx
|   |-- Footer.tsx
|-- public/
|-- styles/
|   |-- globals.css
|   |-- ...
|-- next.config.js
|-- tsconfig.json
|-- package.json

Note: Next.js 13 introduced a new /app directory for handling routing and layouts. Visit here to see how your project structure might change.

The pages directory contains all the Next.js pages that define the routes for our application. The components directory houses reusable React components. The public directory is used to store static assets like images. The styles directory is used for global CSS styles and can be customized based on your project requirements. The next.config.js file allows us to configure various settings for our Next.js app, while the tsconfig.json file provides TypeScript configurations.

Step 3: Running the Development Server

To start the development server and see our Next.js app in action, run the following command:

npm run dev

This will start the development server, and you can access your app at http://localhost:3000 in your web browser.

Congratulations! You’ve successfully set up a new Next.js project with TypeScript.

3. Creating Pages and Components

In a Next.js app, each file under the pages directory becomes a route, and the file name determines the route path.

Note: Next.js 13 introduced a new /app directory for handling routing and layouts. Learn more here.

Here is an example:

Creating A Homepage

Inside the pages directory, create a file named index.tsx:

// pages/index.tsx
import React from 'react';

const HomePage: React.FC = () => {
  return (
    <div>
      <h1>Welcome to Next.js with TypeScript</h1>
      <p>Supercharge your web development!</p>
    </div>
  );
};

export default HomePage;

In this example, we’ve created the homepage with a simple welcome message. The React.FC type is a TypeScript generic that denotes a functional component.

4. Leveraging TypeScript’s Static Typing

One of the main benefits of using TypeScript is its static typing feature. With TypeScript, you can define types for your variables, functions, and components. This helps catch errors early in the development process and provides better code documentation for your team. Let’s see how we can leverage TypeScript’s static typing in our Next.js project.

Typing Props and State

When building React components, it’s essential to specify the types of props and state. TypeScript allows us to define interfaces to represent these types:

// components/Counter.tsx
import React, { useState } from 'react';

interface CounterProps {
  initialValue: number;
}

// In this example, we did not explicit type React.FC as TS can infer it
const Counter = ({ initialValue }: CounterProps) => {
  const [count, setCount] = useState<number>(initialValue);

  const increment = () => {
    setCount((prevCount) => prevCount + 1);
  };

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={increment}>Increment</button>
    </div>
  );
};

export default Counter;

In this example, we’ve defined the CounterProps interface with an initialValue property of type number. The useState hook uses TypeScript generics to specify the type of the state variable count. However, in this case, it is perfectly fine not to use a generic to specify the type of the useState value as Typescript can infer its type.

You are probably wondering why to use interfaces instead of types for typing React components. You will find the answer here.

Typing API Responses

When consuming APIs in your Next.js application, it’s important to define the types of API responses. This helps maintain consistency and ensures that you handle data correctly throughout your app:

// types/Post.ts
interface Post {
  id: number;
  title: string;
  body: string;
}

// components/PostList.tsx
import React, { useEffect, useState } from 'react';
import axios from 'axios';
import { Post } from '../types/Post';

const PostList: React.FC = () => {
  const [posts, setPosts] = useState<Post[]>([]);

  useEffect(() => {
    axios.get<Post[]>('https://jsonplaceholder.typicode.com/posts')
      .then((response) => setPosts(response.data))
      .catch((error) => console.error('Error fetching posts:', error));
  }, []);

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>
          <h3>{post.title}</h3>
          <p>{post.body}</p>
        </li>
      ))}
    </ul>
  );
};

export default PostList;

In this example, we define the Post interface representing a single post’s structure. The PostList component fetches data from an API and stores it in the posts state. We use TypeScript generics in the axios.get call to ensure the correct type of the API response.

5. Managing State with Next.js and TypeScript

State management is a crucial aspect of web development. Next.js provides several options for state management, and TypeScript helps us maintain consistency and catch errors when working with state.

State Management with React Context

React Context is a built-in feature that allows you to manage global state in your application. With TypeScript, you can define types for your context and ensure that the data is correctly typed throughout your components.

// context/ThemeContext.ts
import { createContext, useContext, useState } from 'react';

interface ThemeContextData {
  theme: 'light' | 'dark';
  toggleTheme: () => void;
}

const ThemeContext = createContext<ThemeContextData>({
  theme: 'light',
  toggleTheme: () => {},
});

export const useTheme = () => useContext(ThemeContext);

// components/ThemeToggle.tsx
import React from 'react';
import { useTheme } from '../context/ThemeContext';

const ThemeToggle: React.FC = () => {
  const { theme, toggleTheme } = useTheme();

  return (
    <button onClick={toggleTheme}>
      {theme === 'light' ? 'Switch to Dark Mode' : 'Switch to Light Mode'}
    </button>
  );
};

export default ThemeToggle;

In this example, we create a ThemeContext with the ThemeContextData interface, which defines the theme property as either 'light' or 'dark'. The toggleTheme function allows us to change the theme dynamically. The useTheme hook provides access to the theme context data within our components, and the ThemeToggle component demonstrates how to toggle between light and dark modes.

6. Handling Server-Side Rendering (SSR) and API Routes

Next.js excels at server-side rendering, allowing you to pre-render pages on the server before sending them to the client. TypeScript can help you ensure the correct data types and handling during SSR and API routes.

Server-Side Rendering with getServerSideProps

Next.js provides the getServerSideProps function, which allows you to fetch data on the server side and pass it as props to your page:

// pages/posts/[id].tsx
import { GetServerSideProps } from 'next';
import { useRouter } from 'next/router';
import axios from 'axios';
import { Post } from '../../types/Post';

interface PostPageProps {
  post: Post;
}

const PostPage: React.FC<PostPageProps> = ({ post }) => {
  const router = useRouter();

  if (router.isFallback) {
    return <div>Loading...</div>;
  }

  return (
    <div>
      <h1>{post.title}</h1>
      <p>{post.body}</p>
    </div>
  );
};

export const getServerSideProps: GetServerSideProps<PostPageProps> = async ({ params }) => {
  const { id } = params;
  try {
    const response = await axios.get<Post>(`https://jsonplaceholder.typicode.com/posts/${id}`);
    return { props: { post: response.data } };
  } catch (error) {
    return { notFound: true };
  }
};

export default PostPage;

In this example, we create a dynamic route for individual posts using square brackets in the file name (e.g., [id].tsx). The getServerSideProps function fetches the post data based on the id parameter and passes it as props to the PostPage component. TypeScript allows us to define the type of the fetched post and the component props.

Creating API Routes

Next.js makes it easy to create API routes within your application. These routes can handle server-side logic and data processing. TypeScript allows you to define the types for request and response data, making your API routes more reliable.

// pages/api/posts.ts
import { NextApiRequest, NextApiResponse } from 'next';
import axios from 'axios';
import { Post } from '../../types/Post';

export default async (req: NextApiRequest, res: NextApiResponse<Post[]>) => {
  try {
    const response = await axios.get<Post[]>('https://jsonplaceholder.typicode.com/posts');
    res.status(200).json(response.data);
  } catch (error) {
    res.status(500).json({ error: 'Failed to fetch posts' });
  }
};

In this example, we create an API route named posts that fetches all posts and returns them as a JSON response. TypeScript helps us define the types of the request (NextApiRequest) and the response (NextApiResponse) data.

In this example, we create a DynamicComponent that is only loaded when the user clicks the “Load Dynamic Component” button. TypeScript ensures that the showComponent state is correctly typed as a boolean.

7. Testing Your Next.js Application with TypeScript

Testing is a critical part of the development process. TypeScript helps you catch errors during testing, leading to more robust and reliable applications. Visit here to learn more about testing Next.js/React.js applications.

Unit Testing with Jest and React Testing Library

Jest is a popular testing framework that works seamlessly with TypeScript, allowing you to write unit tests for your components. React Testing Library helps you test React components in a user-centric way.

// components/Counter.test.tsx
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import Counter from './Counter';

test('Counter increments correctly', () => {
  render(<Counter initialValue={0} />);
  const incrementButton = screen.getByText('Increment');

  fireEvent.click(incrementButton);
  fireEvent.click(incrementButton);

  const countDisplay = screen.getByText('Count: 2');
  expect(countDisplay).toBeInTheDocument();
});

In this example, we write a unit test for the Counter component using Jest and React Testing Library. TypeScript ensures that the props passed to the Counter component match the expected type.

Schema Validation with Zod

If you want to validate data for incoming API requests, consider using Zod. Zod is a popular TypeScript-first schema validation library. It allows you to define and validate data schemas, making it easier to ensure that the data in your application adheres to specific shapes and formats.

8. Deploying a Next.js Application with TypeScript

Next.js simplifies the deployment process with built-in features like serverless deployment and static site generation. Deploying a Next.js application with TypeScript is straightforward, and you can choose from various hosting platforms such as Vercel, Netlify, or AWS Amplify.

To deploy your application, build the production version of your Next.js app and then use the deployment platform’s instructions to publish it:

npm run build

This will create an optimized production build of your app in the out directory. You can then follow the deployment platform’s instructions to upload and deploy your app.

9. Best Practices for Next.js and TypeScript

To get the most out of Next.js and TypeScript, consider following these best practices:

  1. Gradual Adoption: If you’re new to TypeScript, consider enabling it incrementally in your project and gradually adding type annotations. This allows you to get familiar with TypeScript while still maintaining a working codebase.
  2. Strict Mode: Enable TypeScript’s strict mode by adding "strict": true to your tsconfig.json file. Strict mode helps catch potential issues and enforce better code quality.
  3. Type Definitions for External Libraries: When using third-party libraries without TypeScript support, install their type definitions using @types/package-name from DefinitelyTyped. This ensures proper type-checking and autocompletion.
  4. Avoid any: Avoid using the any type as much as possible. Instead, use more specific types like unknown, or define custom types to maintain type safety.
  5. Leverage Generics: Take advantage of TypeScript generics when creating reusable components and functions. Generics allow you to write more flexible and type-safe code.
  6. Use Non-Nullable Types: Use the null and undefined type guards or the non-nullable assertion operator (!) to avoid runtime errors caused by unexpected null or undefined values.
  7. Use ESLint with TypeScript: Combine TypeScript with ESLint for enhanced code linting and error checking.

10. Conclusion

Next.js with TypeScript is a powerful combination that enables developers to build scalable and maintainable web applications. By leveraging TypeScript’s static typing, you can catch errors early in the development process and improve your code’s reliability.

Happy coding! 🚀

Share with a friend:

Rajae Robinson

Rajae Robinson is a young Software Developer with over 3 years of work experience building websites and mobile apps. He has extensive experience with React.js and Next.js.

Recent Posts