Next.js with TypeScript — Complete Setup

Sanjeev SharmaSanjeev Sharma
5 min read

Advertisement

Next.js with TypeScript — Complete Setup

TypeScript provides static type checking for Next.js applications, catching errors at compile time and improving developer experience.

Initial TypeScript Setup

Create a new Next.js app with TypeScript:

npx create-next-app@latest my-app --typescript --tailwind

Or add TypeScript to existing project by renaming files to .ts/.tsx.

TypeScript Configuration

Next.js automatically creates tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,
    "strict": true,
    "esModuleInterop": true,
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "noEmit": true,
    "jsx": "preserve",
    "jsxImportSource": "react"
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
  "exclude": ["node_modules"]
}

Typing Pages and Components

Type your pages and components:

// app/page.tsx
import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'Home',
  description: 'Welcome'
}

export default function Home() {
  return <div>Home page</div>
}

Typing Props

Always type component props:

// app/components/post-card.tsx
interface PostCardProps {
  title: string
  excerpt: string
  date: string
  author: string
  featured?: boolean
}

export function PostCard({
  title,
  excerpt,
  date,
  author,
  featured = false
}: PostCardProps) {
  return (
    <article>
      {featured && <span>Featured</span>}
      <h2>{title}</h2>
      <p>{excerpt}</p>
      <footer>
        <span>{author}</span>
        <time>{date}</time>
      </footer>
    </article>
  )
}

Dynamic Route Params

Type params in dynamic routes:

// app/blog/[slug]/page.tsx
interface Params {
  slug: string
}

interface Props {
  params: Params
}

export default async function BlogPost({ params }: Props) {
  const post = await getPost(params.slug)
  return <article>{post.content}</article>
}

Server Component Patterns

Type async Server Components:

// app/dashboard/page.tsx
import type { ReactNode } from 'react'

interface User {
  id: string
  name: string
  email: string
}

async function getUser(): Promise<User> {
  const response = await fetch('/api/user')
  return response.json()
}

export default async function Dashboard(): Promise<ReactNode> {
  const user = await getUser()

  return <div>Welcome, {user.name}</div>
}

Client Component Hooks

Type hooks in Client Components:

// app/components/counter.tsx
'use client'

import { useState, useCallback } from 'react'

export function Counter() {
  const [count, setCount] = useState<number>(0)

  const increment = useCallback((): void => {
    setCount(prev => prev + 1)
  }, [])

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

Form Handling with Types

// app/components/login-form.tsx
'use client'

import { FormEvent, useState } from 'react'

interface FormData {
  email: string
  password: string
}

export function LoginForm() {
  const [formData, setFormData] = useState<FormData>({
    email: '',
    password: ''
  })

  const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
    e.preventDefault()

    const response = await fetch('/api/login', {
      method: 'POST',
      body: JSON.stringify(formData)
    })

    const data = await response.json()
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="email"
        value={formData.email}
        onChange={(e) => setFormData(prev => ({
          ...prev,
          email: e.target.value
        }))}
      />
      <input
        type="password"
        value={formData.password}
        onChange={(e) => setFormData(prev => ({
          ...prev,
          password: e.target.value
        }))}
      />
      <button type="submit">Login</button>
    </form>
  )
}

API Route Typing

// app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server'

interface PostData {
  title: string
  content: string
  author: string
}

export async function POST(request: NextRequest) {
  const body: PostData = await request.json()

  const post = await db.posts.create({
    data: body
  })

  return NextResponse.json(post, { status: 201 })
}

Server Actions with Types

// app/actions.ts
'use server'

import { revalidatePath } from 'next/cache'

interface CreatePostInput {
  title: string
  content: string
}

interface CreatePostResult {
  success: boolean
  postId?: string
  error?: string
}

export async function createPost(
  input: CreatePostInput
): Promise<CreatePostResult> {
  try {
    const post = await db.posts.create({ data: input })
    revalidatePath('/blog')
    return { success: true, postId: post.id }
  } catch (error) {
    return { success: false, error: 'Failed to create post' }
  }
}

Utility Functions

Create typed utility functions:

// lib/auth.ts
import type { Session } from 'next-auth'

export async function getSession(): Promise<Session | null> {
  // Implementation
  return null
}

export function isAuthenticated(session: Session | null): boolean {
  return !!session
}

export function isAdmin(session: Session | null): boolean {
  return session?.role === 'admin'
}

Database Types

Define database schemas:

// lib/db.ts
export interface Post {
  id: string
  title: string
  content: string
  authorId: string
  createdAt: Date
  updatedAt: Date
}

export interface User {
  id: string
  email: string
  name: string
  role: 'user' | 'admin'
}

export interface Comment {
  id: string
  content: string
  postId: string
  authorId: string
  createdAt: Date
}

Strict Mode Settings

Enable strict type checking:

// tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true,
    "noImplicitThis": true,
    "alwaysStrict": true
  }
}

Common Patterns

Pattern 1: Union Types for Variants

type ButtonVariant = 'primary' | 'secondary' | 'danger'

interface ButtonProps {
  variant: ButtonVariant
  onClick: () => void
}

Pattern 2: Generics

interface ApiResponse<T> {
  data: T
  status: number
}

async function fetchData<T>(url: string): Promise<ApiResponse<T>> {
  const response = await fetch(url)
  return response.json()
}

Pattern 3: Discriminated Unions

type Result<T> =
  | { success: true; data: T }
  | { success: false; error: string }

function handleResult<T>(result: Result<T>) {
  if (result.success) {
    console.log(result.data)
  } else {
    console.error(result.error)
  }
}

FAQ

Q: Should I use any type? A: Avoid any. Use unknown and narrow types, or fix the underlying type issue.

Q: How do I type external library components? A: Use @types/package or check library documentation for TypeScript support.

Q: Can I use TypeScript with Pages Router? A: Yes, use pages/ directory with TypeScript. New projects should use App Router.


TypeScript transforms Next.js development into a safer, more productive experience.

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro