Published on June 24, 2024
Migrating your Next.js projects from JavaScript to TypeScript enhances your code quality and developer experience by improving debugging, adding auto-completion and linting, and making your code easier to understand (to name just a few benefits).
This tutorial gives you a clear, step-by-step path to using TypeScript with Next.js, including how to create new projects and how to integrate TypeScript into existing Next.js projects.
TypeScript is a programming language that builds on the versatility of JavaScript by adding static typing and other developer-friendly features. TypeScript applications are compiled to plain JavaScript before they can be run (either in the browser, or as mobile apps), making TypeScript backwards compatible with existing JavaScript environments, while being able to add new features.
Static typing checks are performed when TypeScript code is compiled, so that errors are flagged before the application can run, unlike dynamically typed languages like JavaScript that only raise issues (or perform unexpectedly) at runtime — often after an application has been deployed to users. This solves one of the biggest problems in JavaScript development: bugs or unexpected input result in a mistyped variable, leading to your application mishandling it.
For example, numbers wrongly being stored as strings results in arithmetic operations concatenating the values instead — think of the difference between "1" + "1" = "11" and 1 + 1 = 2. Enforcing types on variables prevents this from happening, instead resulting in an error that must be rectified during development.
Explicit type annotations, which define the type of each variable upfront, protect against ambiguous variables, but these are not mandatory: TypeScript will still infer a type if you don't supply one and do type checking on that type, just as JavaScript does.
Some examples of explicit type annotations:
const name: string = "John"; // name is of type 'string'
const age: number = 28; // age is of type 'number'
let isLoading: boolean = true; // isLoading is of type 'boolean'
TypeScript makes your code more reliable, as its static typing allows for earlier detection of bugs and the minimization of runtime errors. You're also less likely to introduce new bugs during code refactoring for the same reasons.
Your code will be much more readable once you add strict type annotations. Being explicit about types makes your code much easier to understand — especially for new team members who will benefit from the additional context (or yourself if you're revisiting code you haven't worked on in a while).
TypeScript's aliases and interfaces allow you to define how particular objects are structured and what properties they have. This acts as a form of documentation, making the intention and purpose of code clear to the developer.
TypeScript can provide exact type information to your IDE (like Visual Studio Code), meaning that the IDE's suggestions for methods, properties, and so on, are much more accurate than they are when providing hints for JavaScript. This speeds up development time by making code easier to navigate, and reduces the number of errors in your code.
These features make TypeScript projects easier to maintain, and greatly improve the reliability of deployed applications.
To add TypeScript to an existing Next.js project requires a few extra steps.
Install TypeScript: In your project's root directory, run:
npm install -–save-dev typescript
Install type definitions for React and Node: Type definitions provide additional type information about JavaScript libraries so they can be used as if they were TypeScript libraries. This information is stored in declaration files with a .d.ts suffix. As Next.js provides both server-side functionality using Node.js and front-end functionality using React, you'll need type definitions for both:
npm install --save-dev @types/react @types/react-dom @types/node
Create and populate the tsconfig.json
file: This is a configuration file that tells the TypeScript compiler how to compile your code. First, create an empty file named tsconfig.json
and then run the development server with npm run dev. This will trigger Next.js to populate tsconfig.json
with the configuration data it needs.
Rename *.js
and *.jsx
files to *.ts
and *.tsx
: For .jsx
files, these should always be renamed to .tsx
. For .js
files, you need to look at the contents of each file before deciding how to rename it. If it contains any JSX, rename it to have a .tsx
suffix, if not, rename it to .ts
. Note that it's possible to do this incrementally as you update the code in each file to TypeScript, making migrations on large codebases a less daunting task.
If you've missed converting some files, your project won't compile, but you'll be able to see an error message specifying which file is causing the issue.
Don't rename any files that have been auto-generated (such as those in .next/
) or which are external libraries or installed dependencies (such as those in node_modules/
).
That's it! You can now compile and run your code with npm run dev
. Your build process should continue to run as-is, as the Next.js compilation command is the same for both JavaScript and TypeScript.
The great thing about TypeScript being a superset of JavaScript is that you can leave your existing JavaScript code how it is, and it will still work. This means that you can update your code to TypeScript according to your own timeline, in small increments, or even choose to just add type safety only to new code or to a few select components.
Once you're up and running with TypeScript and Next.js, here are some tips to make sure you're taking full advantage of TypeScript's features to improve your development processes and app reliability.
Specifying the data type of a variable, function, parameter, or return value reduces the possibility of a runtime error.
In the example below, not specifying the data type of age means that the string "twenty six" can be assigned to it, which then leads to a NaN (not a number) value being returned from the doubleAge() function. The resulting NaN value is of no use, resulting in bugs if the problem isn't caught and handled with additional code.
Adding a data type to the variable declaration will cause a compile-time error (or, if your IDE supports it, flag the error in your code before you compile) if any code allows non-numeric values to be assigned to the age variable, meaning you can fix it before it becomes a problem:
An interface is a contract that defines the ”shape” of an object, i.e. how many properties it has and what data types they are.
Without this form of type safety, a User
object could be created with any properties at all, and your code may not be written to be able to handle it.
In the example below, you receive a compile-time error complaining that lastName
is missing from the newUser
as it's expected by the User
type.
Without an interface defining which properties should exist on the User
type, this error would not be flagged at compile time. Instead, the console would print "Hello John undefined" — not the intended behavior end users should be encountering.
Next.js's preview mode allows you to render the page at request time instead of build time. This is useful if you're using a headless CMS as it lets you preview drafts before publishing.
DefinitelyTyped helps you when you need to use a plain JavaScript library but you want the type safety that TypeScript normally provides. It's a NPM package containing a repository of TypeScript type definitions for popular JavaScript libraries and frameworks that don't have their own type definitions, maintained by the TypeScript community.
You can access the DefinitelyTyped types under the @types
namespace — for example, if you wanted to add type definitions for lodash
, you could do that with the npm install @types/lodash --save-dev
command.
Next.js has type safety built in (as it's developed in TypeScript), but you can only take full advantage of it if you're using TypeScript in the code you build on it.
For example, NextApiRequest
and NextApiResponse
are interfaces that provide convenience methods for handling API routes with Next.js. An example of TypeScript making use of NextApiRequest
's built-in type safety is below:
When this code is compiled, a compile-time error is flagged on req.headers.get("user-agent")
to inform us that req.headers.get
is possibly undefined. Once an error is flagged, you can check the documentation and find out why it failed. In this case, the error is because there is no .get()
method on req.headers
— the correct code for reading a specific header is actually req.headers['user-agent']
.
If you were to do this using plain JavaScript instead, you would not be warned about the issue prior to running the code, and instead get a runtime error when the code tries to incorrectly call req.headers.get('user-agent')
.
Contentful further enhances your developer experience when used with TypeScript and Next.js. The Contentful REST API and GraphQL API deliver your content across different channels, including websites, smartphone apps, IoT, and games. Next.js and TypeScript allow you to make asynchronous, type-safe requests to these APIs, resulting in super fast, seamless, and reliable user experiences.
Contentful allows you to completely decouple the content of your app from the presentation layer, delivering all of your blog posts, images, media, and other content to your users from the edge. You can give up writing your own content hosting and management back ends, and instead define your own content types and data schemas in Contentful to match your use case.
Subscribe for updates
Build better digital experiences with Contentful updates direct to your inbox.