·

4 min read

☄️ Integrating Typescript with Next.js

Get easy type inference from HTTP API responses

Guide

Typescript

Info

This post is specifically aimed at Prisma users but you should be able to use it with any ORM (or no ORM ot all if you roll that way).

One of the most annoying things about integrating different technologies together is losing static typing along the way. This is my biggest complaint when it comes to working with databases without an ORM, but is also a big issue with HTTP APIs. GraphQL and GraphQL Code Generator solve this problem beautifully, but GraphQL can still be very frustrating to work with from time to time due to its immature ecosystem. Sometimes you just want to be able to use HTTP and call it a day.

Next.JS is an amazing technology that allows us to use Typescript, but even with static typing and an ORM that has static definitions for things like a User stored in the database, that User is usually not the same User you send to the frontend as API responses so often times you end up losing that static typing along the way.

Thankfully, to solve this problem, Next.js lets us share code between our frontend and the backend where we actually don’t have to lose the type information along the way.

Here’s an example of thow that’s possible:

/pages/api/user/[userId].ts

Typescript

1import { NextApiRequest, NextApiResponse } from "next"
2import { db } from "../your-database-file"
3
4const response = (id: string) => {
5 return db.user.findUnique({
6 where: { id },
7 })
8}
9
10export default async (req: NextApiRequest, res: NextApiResponse) => {
11 const { userId } = req.query
12
13 res.json(await response(userId))
14}
15
16export type UserResponse = PromiseReturnType<typeof response>

Here we’re querying the database with Prisma which generates type information for exactly the shape of data you’re querying for the database which is why the return value isn’t explicit in the response function. It’s already inferred with the exact return value. There is a small issue however, which is that the type is actually Promise<Image> and not Image, in order to get the Image type out, we have to unwrap the Promise type. If you’re a Prisma user, then you can simply use a type-level function it exports called PromiseReturnType

Typescript

import { PromiseReturnType } from "@prisma/client"

If not, you can use some manual Typescript magic instead.

export type PromiseReturnType<T> = ReturnType<T> extends Promise<infer R>
? R
: never

If you’ve never looked into the infer keyword or conditional types, I highly recommend you do, they’re fantastic.

This way we can declare the types of the responses we get from an endpoint inside the endpoint file itself which makes importing much easier and also enables using type inference for database results from ORMs that are statically typed without repeating the typings.

All of this sounds pretty good so far, but when it comes to importing things, some of you might be wondering:

You, reading thisToday at 4:20 PM

Hold on, am I going to have to import an API endpoint file in my frontend now? This is going to create issues importing backend code for sure.

7

12

But not to worry, as of Typescript 3.8, we can import just types from a typescript file, meaning it’s possible to do this:

/pages/home.tsx

Typescript

import type { HomeResponse } from "./api/home.ts"

Making sure that you don’t accidentally bring in an entire backend bundle to your frontend when trying to share types.

This is all nice and everything, but what if we could write a function that could do the work of wrapping a function that returns a promise and replying with the response value, maybe something like:

1import { NextApiRequest, NextApiResponse } from "next"
2
3type Handle<T> = (req: NextApiRequest, res: NextApiResponse) => Promise<T>
4
5function respond<T>(f: Handle<T>) {
6 return async (req: NextApiRequest, res: NextApiResponse) => {
7 try {
8 const result = await f(req, res)
9 res.json(result)
10 } catch (err) {
11 res.statusCode = 500
12 res.json({ message: err })
13 }
14 }
15}
16
17const handle: Handle = (req, res) => {
18 return db.users.findMany({})
19}
20
21export type UsersResponse = PromiseReturnType<typeof handle>
22
23export default respond(handle)

Unfortunately, this doesn’t work because of this issue about generic values. Otherwise we would be able to get the type inference working flawlessly. Typescript core team pls…