Next.js with TypeScript — Complete Setup
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.
- Next.js with TypeScript — Complete Setup
- Initial TypeScript Setup
- TypeScript Configuration
- Typing Pages and Components
- Typing Props
- Dynamic Route Params
- Server Component Patterns
- Client Component Hooks
- Form Handling with Types
- API Route Typing
- Server Actions with Types
- Utility Functions
- Database Types
- Strict Mode Settings
- Common Patterns
- FAQ
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