Ibrahima Ndaw JavaScript enthusiast, full-stack developer, and blogger who also dabbles in UI/UX design.

Using Next.js with TypeScript

6 min read 1783

Using Next.js with TypeScript

Editor’s note: This post was updated on 15 November 2021 to update outdated information and provide additional clarity about TypeScript types.

Next.js allows you to build static and dynamic (server-side) apps using React. It ships with handy functionalities such as:

  • API routes
  • File-system routing
  • Image optimization
  • Middlewares
  • ES Modules support and URL Imports
  • Server components
  • HTTP streaming
  • Next.js Live

Next.js is built with TypeScript under the hood, so you get better IntelliSense and type definitions in your editor by default with just JavaScript. But when you couple that with TypeScript, you can get an even better developer experience — including instant feedback when your component expects props, but you didn’t pass any.

You’re also able to build with Next’s exported types and define your own to build with across your applications. These types help give your code better structure by dictating what your objects, arrays, etc., look like ahead of time. That way, you, your code editor, and any developer after you knows how to reference your code.

Next’s features make building full-stack React apps easier than ever, from simplified routing to performance-enhancing features like image optimization.

In this tutorial, we’ll demonstrate how to use Next.js with TypeScript and introduce you to an exciting and modern stack for building high-quality, search-optimized, and predictable apps. We’ll cover the following in detail:

To show Next.js and TypeScript in action, we’ll walk through how to build a simple article manager app. Our example app will retrieve data from JSON placeholder.

What is Next.js?

Next.js is a production-ready framework built on top of React and Node.js. It ships with all the features listed above and more.

You can use Next.js to build static and dynamic apps since it supports both client- and server-side rendering. Next.js 9 introduced API routes, which allow you to extend your Next app with a real backend (serverless) built with Node.js, Express.js, GraphQL, and so on. The most recent version at the time of writing is Next.js 12, which has a new, much faster Rust compiler.

Next.js uses automatic code-splitting (lazy loading) to render only the JavaScript needed for your app. Next.js can also pre-render your pages at build time to serve on demand, which can make your app feel snappy because the browser does not have to spend time executing the JavaScript bundle to generate the HTML for your app, making it possible for more search engine crawlers to index your app, which in turn is great for SEO.

We made a custom demo for .
No really. Click here to check it out.

What is TypeScript?

TypeScript is a popular language created and maintained by Microsoft. It’s a superset of JavaScript, which means all valid JavaScript is valid Typescript.

You can convert your existing JavaScript app to TypeScript and it should work as expected, as long as your code is valid JavaScript. TypeScript allows you to set types on your variables and functions so you can type-check your code statically and catch errors at compile time.

You can also use modern features that are not yet supported in JavaScript. And don’t worry about browser support — TypeScript compiles to plain JavaScript, which means your TypeScript code will never ship in the browser.

Using TypeScript in a Next.js app

To create a new Next.js app, you can use Create Next App.

Begin by opening your command-line interface (CLI) and running the command below:

npx create-next-app next-typescript-example

The command will generate a fresh Next.js app. Now, let’s structure the project as follows:

src
├── components
|  ├── AddPost.tsx
|  └── Post.tsx
├── pages
|  ├── index.tsx
|  └── _app.tsx
├── styles
|  └── index.css
├── tsconfig.json
├── types
|  └── index.ts
├── next-env.d.ts
└── package.json

To enable TypeScript in a Next.js app, add a tsconfig.json file to the root of the project. Next.js will recognize the file and use TypeScript for the project.

With this in place, we can now create files with .ts or .tsx extensions. Next.js handles the compilation of the TypeScript code to JavaScript and then serves our app as usual in the browser.

Creating TypeScript types in Next.js

You can create types for anything in your application, including prop types, API responses, arguments for your utility functions, and even properties of your global state!

We first create a type for our posts. The interface below reflects the shape of a Post object. It expects id, title, and body properties.

// types/index.ts

export interface IPost {
  id: number
  title: string
  body: string
}

Creating components in Next.js

Now that the post type (IPost) is ready for use, let’s create the React components and set the types.

// components/AddPost.tsx

import * as React from 'react'
import { IPost } from '../types'

type Props = {
  savePost: (e: React.FormEvent, formData: IPost) => void
}

const AddPost: React.FC<Props> = ({ savePost }) => {
  const [formData, setFormData] = React.useState<IPost>()

  const handleForm = (e: React.FormEvent<HTMLInputElement>): void => {
    setFormData({
      ...formData,
      [e.currentTarget.id]: e.currentTarget.value,
    })
  }

  return (
    <form className='Form' onSubmit={(e) => savePost(e, formData)}>
      <div>
        <div className='Form--field'>
          <label htmlFor='name'>Title</label>
          <input onChange={handleForm} type='text' id='title' />
        </div>
        <div className='Form--field'>
          <label htmlFor='body'>Description</label>
          <input onChange={handleForm} type='text' id='body' />
        </div>
      </div>
      <button
        className='Form__button'
        disabled={formData === undefined ? true : false}
      >
        Add Post
      </button>
    </form>
  )
}

export default AddPost

As you can see, we start by importing the IPost type. After that, we create another type named Props that mirrors the props received as a parameter by the component.

Next, we set the type IPost on the useState Hook. Then, we use it to handle the form data. Once the form is submitted, we rely on the function savePost to save the data on the array of posts.
Now, we can create and save a new post.

Let’s move on to the component responsible for displaying the Post object.

// components/Post.tsx

import * as React from 'react'
import { IPost } from '../types'

type Props = {
  post: IPost
  deletePost: (id: number) => void
}

const Post: React.FC<Props> = ({ post, deletePost }) => {
  return (
    <div className='Card'>
      <div className='Card--body'>
        <h1 className='Card--body-title'>{post.title}</h1>
        <p className='Card--body-text'>{post.body}</p>
      </div>
      <button className='Card__button' onClick={() => deletePost(post.id)}>
        Delete
      </button>
    </div>
  )
}

export default Post

This Post component receives the post object to show and a deletePost function as props. The arguments have to match the Props to make TypeScript happy.

We are now able to add, show, and delete posts. Let’s import the components into the App.tsx file and create the logic to handle the posts.

import * as React from 'react'
import { InferGetStaticPropsType } from 'next'
import AddPost from '../components/AddPost'
import Post from '../components/Post'
import { IPost } from '../types'

const API_URL: string = 'https://jsonplaceholder.typicode.com/posts'

export default function IndexPage({
  posts,
}: InferGetStaticPropsType<typeof getStaticProps>) {
  const [postList, setPostList] = React.useState(posts)

  const addPost = async (e: React.FormEvent, formData: IPost) => {
    e.preventDefault()
    const post: IPost = {
      id: Math.random(),
      title: formData.title,
      body: formData.body,
    }
    setPostList([post, ...postList])
  }

  const deletePost = async (id: number) => {
    const posts: IPost[] = postList.filter((post: IPost) => post.id !== id)
    console.log(posts)
    setPostList(posts)
  }

  if (!postList) return <h1>Loading...</h1>

  return (
    <main className='container'>
      <h1>My posts</h1>
      <AddPost savePost={addPost} />
      {postList.map((post: IPost) => (
        <Post key={post.id} deletePost={deletePost} post={post} />
      ))}
    </main>
  )
}

export async function getStaticProps() {
  const res = await fetch(API_URL)
  const posts: IPost[] = await res.json()

  return {
    props: {
      posts,
    },
  }
}

In this component, we first import the types and components created earlier. The type InferGetStaticPropsType, provided by Next.js, allows us to set the type on the method getStaticProps. It will infer the type defined on the props returned by getStaticProps.

After that, we initialize the state with the posts array using the useState Hook. Next, we declare the function addPost to save the data on the array of posts. The deletePost method receives as an argument the id of the post, which allows us to filter the array and remove the post.

Finally, we pass in the expected props to the components. Then, we loop through the response data and display it using the Todo component. The data is retrieved from the JSON placeholder API with the help of the getStaticProps method provided by Next.js.

Setting the type of posts to be an array of objects that have the structure defined by IPost helps us and our editor know exactly what fields are available to us from the API’s response.

You can alternatively use the getServerSideProps method, Fetch, or a library to fetch the data. It’s just a matter of how you want to render your Next.js app.

In this demo, we render our app by statically generating the pages, which means Next.js generates HTML files with little JavaScript at build time and the same HTML file is served on each request. Statically generating your pages is the recommended way to serve your app because of the performance benefits from serving pre-generated HTML files.

Server-side rendering is also an option, this method generates a fresh HTML file everytime a request is made to the server. This is the mode you would use getServerSideProps for.

Testing your Next.js app

With this final touch, the app is ready to be tested on the browser. Begin by locating the root of the project and running this command:

yarn dev

Or, if using npm:

npm run dev

If everything works as expected, you should see the Next app at http://localhost:3000/:

A preview of our Next.js app
And that’s it!

Conclusion

In this tutorial, we covered how to use TypeScript with Next.js by building an article manager app. You can view the finished project on GitHub.

Next.js has really good support for TypeScript and is easy to set up. That makes it simple to build strongly typed React apps with Next.js and TypeScript that run on either the client or the server. To put it plainly, Next.js and TypeScript is a very exciting stack to try on your next React project.

LogRocket: Full visibility into production Next.js apps

Debugging Next applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Next app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your Next.js apps — .

Writing a lot of TypeScript? Watch the recording of our recent TypeScript meetup to learn about writing more readable code.

TypeScript brings type safety to JavaScript. There can be a tension between type safety and readable code. Watch the recording for a deep dive on some new features of TypeScript 4.4.

Ibrahima Ndaw JavaScript enthusiast, full-stack developer, and blogger who also dabbles in UI/UX design.

One Reply to “Using Next.js with TypeScript”

  1. This is good but how can we define types globally so that we don’t need to keep importing them each time?

Leave a Reply