Editor’s note: This guide to using Next.js with TypeScript was last updated on 24 April 2023 to reflect recent updates and add sections to clarify why you should use Next.js with TypeScript, how to add TypeScript to existing projects, and how to make your types global. To learn more, refer to our guide on types vs. interfaces in TypeScript.
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, middleware, ES Modules support and URL imports, server components, HTTP streaming, and 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 know how to reference your code. Next.js’ 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. 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. We’ll cover the following in detail:
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 client and server-side rendering. Next.js v9 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.
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. This makes it possible for more search engine crawlers to index your app, which is great for SEO.
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 Next.js with TypeScript is a powerful combination that offers a range of benefits for developers, ultimately enhancing their experience and the quality of the applications they create. Here’s why you should consider using Next.js with TypeScript, accompanied by some feelings and examples.
TypeScript adds static types to JavaScript, significantly reducing the chances of introducing bugs and making the code more maintainable. When working with Next.js, TypeScript’s type-checking capabilities enable developers to feel more confident in their code, as they can catch potential errors early in the development process. Imagine you have a blog application that receives data from an API. With TypeScript, you can create interfaces for the expected data structure, ensuring that the received data matches the expected format, and that any discrepancies are caught during development.
TypeScript’s static types and Next.js’s built-in features, like hot reloading and automatic routing, work together to provide a seamless and enjoyable developer experience. With TypeScript, developers can feel a sense of relief when working with complex applications, as the type information guides them through the code, making it easier to understand and navigate.
For example, when using TypeScript with Next.js, features like autocompletion and type inference in code editors like VS Code significantly improve the developer experience. This assistance can boost productivity, as developers spend less time figuring out the correct syntax or searching for the right function.
Both Next.js and TypeScript are designed to support large-scale applications, making them a perfect match for growing projects. TypeScript’s type system enforces a SOLID structure that promotes best practices, making it easier to scale the codebase without compromising on maintainability or quality. Additionally, Next.js offers features like automatic code-splitting and server-side rendering, which help build high-performing applications.
Suppose you are working on a large ecommerce application with multiple developers contributing to the project. With TypeScript and Next.js, it becomes easier to manage the growing complexity of the codebase, as the enforced structure and built-in features ensure that the application remains performant and maintainable.
Next.js and TypeScript enjoy strong community support, with a wealth of resources, libraries, and third-party tools available to developers. This thriving ecosystem makes it easy to find solutions to common problems and integrate with other technologies. Many popular libraries, such as Material UI, TanStack Query, and styled-components, provide TypeScript typings inbuilt or via @types
packages. This support enables developers to quickly integrate these libraries into their Next.js projects, ensuring type safety and a more robust application.
To create a new Next.js app, you can use Create Next App. Begin by opening your CLI and running the command below:
npx create-next-app next-typescript-example
The above command will take you through the following prompts:
✔ Would you like to use TypeScript with this project? … No / Yes
: Because we are uisng TypeScript for this app, we’ll select Yes
✔ Would you like to use ESLint with this project? … No / Yes
: Select Yes
to use ESlint✔ Would you like to use Tailwind CSS with this project? … No / Yes
: **Select Yes to use Tailwind CSS for styling the application
✔ Would you like to use src/ directory with this project? … No / Yes ?
: **We’ll select Yes to use the src folder in the application
✔ Would you like to use experimental app/ directory with this project? › No /Yes ?
: **For this tutorial, we’ll stick to the traditional pages
folder for this application✔ What import alias would you like configured? … @/*?
: Enter @/*
to use the import alias to import modules into the applicationHere’s what it should look like:
After the above prompts, the command will generate a fresh Next.js app and install the required dependencies. Next, change the directory into the project folder with the command below:
cd next-typescript-example
Now, let’s structure the project as follows:
src
├── components
| ├── AddPost.tsx
| └── Post.tsx
├── pages
| ├── index.tsx
| └── _app.tsx
| └── _document.tsx
├── styles
| └── globals.css
├── tsconfig.json
├── types
| └── globals.d.ts
| └── index.ts
|.
├── next-env.d.ts
└── package.json
Because we chose to use TypeScript for this application during the project setup, a tsconfig.json
file has been created in 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.
Before we create our Next.js application, let’s look at how you can add TypeScript to your existing Next.js project. First, install the required dependencies with the command below:
npm install --save-dev typescript @types/react @types/node
Now, let’s look at what each of these dependencies does:
typescript
: This is the main TypeScript package that adds TypeScript support to your project@types/react
: This package provides TypeScript type definitions for React. It allows you to use TypeScript with React and ensures that your code is type-safe@types/node
: This package provides TypeScript type definitions for Node.js. It allows you to use TypeScript with Node.js and ensures that your code is type-safeNext, create a tsconfig.json
file in the root directory of your project that will contain the TypeScript configurations for your project. Add the configurations below to the tsconfig.json
file:
{ "compilerOptions": { "target": "es5", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, "allowSyntheticDefaultImports": true, "strict": true, "forceConsistentCasingInFileNames": true, "module": "esnext", "moduleResolution": "node", "resolveJsonModule": true, "isolatedModules": true, "noEmit": true }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], "exclude": ["node_modules"] }
Let me clarify what these configurations do:
target
: This specifies the version of JavaScript that the TypeScript compiler should output. In this case, it is set to ES5lib
: Specifies the libraries that are available to your code. In this case, it includes the DOM API, iterable objects, and the latest version of ECMAScriptallowJs
: This specifies whether the TypeScript compiler should allow JavaScript filesskipLibCheck
: This specifies whether the TypeScript compiler should skip type checking of declaration filesesModuleInterop
: This specifies whether the TypeScript compiler should allow default imports from modules with no default exportallowSyntheticDefaultImports
: This specifies whether the TypeScript compiler should allow default imports of modules that do not have a default exportstrict
: This specifies whether the TypeScript compiler should enforce strict type checkingforceConsistentCasingInFileNames
: This specifies whether the TypeScript compiler should enforce consistent casing of filenamesmodule
: This specifies the module format that the TypeScript compiler should use. In this case, it is set to ES modulesmoduleResolution
: This specifies the algorithm that the TypeScript compiler should use to resolve module dependencies. In this case, it is set to node
to use Node.js module resolutionresolveJsonModule
: This specifies whether the TypeScript compiler should allow importing JSON filesisolatedModules
: This specifies whether each file should be treated as a separate modulenoEmit
: This specifies whether the TypeScript compiler should emit any output filesThe include
and exclude
options in the tsconfig.json
file determine which files should be included or excluded from the TypeScript compilation process. In this case, we have configured the compiler to include all .ts
and .tsx
files in the project while excluding the node_modules
directory. Also, the next-env.d.ts
file is a specific file that Next.js uses to declare global types for the project, and it is included as a part of the TypeScript compilation process.
Next, update the content of the file next.config.js
file to use TypeScript with the code snippet below:
module.exports = { webpack(config) { config.resolve.extensions.push('.ts', '.tsx'); return config; } };
This configuration tells Next.js to resolve files with the .ts
and .tsx
extensions. Finally, name all your .jsx
and .js
files to .ts
, and .ts
files and update your code to TypeScript syntax.
Types are crucial in TypeScript. They provide a solid foundation for maintainable code, catching errors, and improving the developer experience. With custom types in Next.js, you’ll harness TypeScript’s full potential, resulting in more reliable code. And, depending on the use case, 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! First, let’s 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 }
However, this method may not be handy because we’ll import our components each time we need to use it. So, let’s learn how to make our types global.
Creating global types in our Next.js application simplifies the work, and you can use them throughout the project without importing them in each file. It feels seamless to access these types, which cover various application aspects like prop types, API responses, or global state properties.
First, create a types
directory in the root of your Next.js project, create a types directory. Then, inside the types
directory, create a new file with a global.d.ts
extension, such as global.d.ts
. This file will hold all your global type declarations. Then, add the IPost
type to the global.d.ts
file, to declare a global interface for a post with the code snippet below:
// types/global.d.ts export {} declare global { interface IPost { id: number title: string body: string } }
In the above code, we employed the export {}
line within our global.d.ts
file, designating it as an external module. This approach allows us to efficiently augment the global scope.
Finally, update tsconfig.json
to inform TypeScript about the global type declaration file, as shown below:
{ "compilerOptions": { ... "typeRoots": ["./types", "./node_modules/@types"] }, ... }
Now that the post type (IPost
) is ready for use, let’s create the React components and set the types, as shown below:
// components/AddPost.tsx import * as React from 'react' 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((prevState) => ({ ...prevState, [e.currentTarget?.id]: e.currentTarget?.value, } as IPost)); }; return ( <form className='Form' onSubmit={(e) => savePost(e, formData as IPost)}> <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 are using the IPost
type without importing it into the file. 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. Refer to the following code:
// components/Post.tsx import * as React from 'react' 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 index.tsx
file and create the logic to handle the posts:
import * as React from 'react' import AddPost from '@/components/AddPost' import Post from '@/components/Post' import { InferGetStaticPropsType } from 'next' const API_URL: string = 'https://jsonplaceholder.typicode.com/posts' export default function Home({ 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 use the types
and import the 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 of serving pre-generated HTML files.
Server-side rendering is also an option; this method generates a fresh HTML file whenever a request is made to the server. This is the mode you would use getServerSideProps
for.
Now, let’s add some styling to the application to make it virtually appealing to the users. To that, copy the styles here to the styles/globals.css
file.
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/
, as shown below:
That’s it!
To implement TypeScript in Next.js API routes, we need to create our API routes in the pages/api
directory. For example, let’s create a simple API route that returns a JSON response with a message:
// pages/api/hello.ts import { NextApiRequest, NextApiResponse } from 'next' export default function handler(req: NextApiRequest, res: NextApiResponse) { res.status(200).json({ message: 'Hello, world!' }) }
In the above code snippet, we’ve imported NextApiRequest
and NextApiResponse
types from next, which provide type checking for the request
and response
objects. We’ve also added a type annotation to the handler function, indicating that it expects a NextApiRequest
object as its first parameter and a NextApiResponse
object as its second parameter.
Using TypeScript with Next.js brings numerous advantages, making your development journey more enjoyable and productive. First, TypeScript’s static typing system helps you catch errors early, making it easier to maintain and refactor your codebase. It feels reassuring to have the compiler notify you of potential issues before they become problems in production.
With TypeScript, you’ll benefit from better autocompletion, code navigation, and refactoring features in modern IDEs. It’s a delight to work with code that’s easier to understand and navigate. TypeScript also ensures that your code adheres to the defined types, making your application more reliable and less prone to runtime errors. This added security brings peace of mind, as you know your application is better protected against unforeseen issues.
For instance, imagine you’re building a Next.js application that retrieves data from a REST API and want to display a list of products. With TypeScript, you can define a Product
interface, ensuring that your components consistently handle product data:
typescriptCopy code interface Product { id: number; name: string; description: string; price: number; }
Using this Product
interface in your components and API calls ensures that your application always expects the correct data structure. This makes it easier to catch any discrepancies between the expected data and what the API returns, increasing the overall reliability of your application.
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. Overall, Next.js has good support for TypeScript and is easy to set up. That makes building strongly typed React apps with Next.js and TypeScript that run on either the client or the server simple. Simply put, Next.js and TypeScript is a very exciting stack to try on your next React project.
LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile 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.js 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 — start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowCreate a multi-lingual web application using Nuxt 3 and the Nuxt i18n and Nuxt i18n Micro modules.
Use CSS to style and manage disclosure widgets, which are the HTML `details` and `summary` elements.
React Native’s New Architecture offers significant performance advantages. In this article, you’ll explore synchronous and asynchronous rendering in React Native through practical use cases.
Build scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
2 Replies to "Using Next.js with TypeScript"
This is good but how can we define types globally so that we don’t need to keep importing them each time?
You should clarify that this blog uses the Pages Router and not the App Router. According to the docs, the App Router is the default router in 13.4.19