Module 17 — TypeScript in Next.js
TypeScript is an essential part of modern Next.js development.
Next.js supports TypeScript directly, allowing you to add static types to:
- Components
- Props
- API responses
- Server functions
- Client functions
- Database results
- Forms
- Route parameters
- Service functions
- Async operations
A good TypeScript architecture catches many mistakes before your application runs.
The basic idea is:
1JavaScript 2 ↓ 3Add Types 4 ↓ 5TypeScript 6 ↓ 7Compile-time Safety 8 ↓ 9More Reliable Next.js Application
Why TypeScript Matters in Next.js
Consider a JavaScript object:
1const course = { 2 id: "101", 3 title: "Next.js", 4 price: 499, 5};
You know what the object contains, but TypeScript can make that structure explicit.
1type Course = { 2 id: string; 3 title: string; 4 price: number; 5};
Now:
1const course: Course = { 2 id: "101", 3 title: "Next.js", 4 price: 499, 5};
If you accidentally write:
1const course: Course = { 2 id: "101", 3 title: "Next.js", 4 price: "499", 5};
TypeScript reports an error because:
1Expected: 2number 3 4Received: 5string
This is especially useful in large Next.js applications where data moves through many layers.
TypeScript and Next.js Architecture
A real application may have this flow:
1Database 2 ↓ 3Backend API 4 ↓ 5API Response 6 ↓ 7Service Function 8 ↓ 9Server Component 10 ↓ 11Client Component
TypeScript can define the structure at every stage:
1Database Result 2 ↓ 3Course Type 4 ↓ 5API Response Type 6 ↓ 7Service Return Type 8 ↓ 9Component Props
This makes data flow much easier to understand.
1. Type Aliases
A type alias defines the structure of a value.
1type Course = { 2 id: string; 3 title: string; 4 description: string; 5 price: number; 6};
Now you can use it:
1const course: Course = { 2 id: "101", 3 title: "Next.js", 4 description: "Learn Next.js", 5 price: 499, 6};
This is one of the most common TypeScript patterns in Next.js.
Type Aliases for Component Props
Suppose a component receives a course:
1type Course = { 2 id: string; 3 title: string; 4 price: number; 5}; 6 7type CourseCardProps = { 8 course: Course; 9}; 10 11export function CourseCard({ 12 course, 13}: CourseCardProps) { 14 return ( 15 <article> 16 <h2>{course.title}</h2> 17 <p>₹{course.price}</p> 18 </article> 19 ); 20}
Now the component knows exactly what it expects.
2. Interfaces
Interfaces can also describe object structures.
1interface Course { 2 id: string; 3 title: string; 4 description: string; 5 price: number; 6}
You can use it exactly like a type:
1const course: Course = { 2 id: "101", 3 title: "Next.js", 4 description: "Learn Next.js", 5 price: 499, 6};
Type vs Interface
Both are useful.
For example:
1type Course = { 2 id: string; 3 title: string; 4};
and:
1interface Course { 2 id: string; 3 title: string; 4}
are both valid ways to describe an object.
A practical approach is to use either consistently across your project.
Type aliases are particularly convenient for:
1Union types 2Primitive aliases 3Tuples 4Mapped types 5Combinations of types
Interfaces are useful when modeling extendable object contracts.
3. Extending Interfaces
Interfaces can extend other interfaces.
1interface BaseEntity { 2 id: string; 3 createdAt: string; 4} 5 6interface Course extends BaseEntity { 7 title: string; 8 price: number; 9}
Now Course contains:
1id 2createdAt 3title 4price
Example:
1const course: Course = { 2 id: "101", 3 createdAt: "2026-08-23", 4 title: "Next.js", 5 price: 499, 6};
4. Generics
Generics allow you to write reusable code that works with different types.
A simple example:
1function identity<T>(value: T): T { 2 return value; 3}
Now TypeScript can infer the type:
1const name = identity("Ankit");
Here:
1T = string
Another example:
1const number = identity(100);
Here:
1T = number
The same function works with both.
Generic API Responses
Generics become extremely useful when building API clients.
Define:
1type ApiResponse<T> = { 2 data: T; 3 message: string; 4 success: boolean; 5};
Now you can create:
1type Course = { 2 id: string; 3 title: string; 4};
Then:
1type CourseResponse = 2 ApiResponse<Course>;
And:
1type CourseListResponse = 2 ApiResponse<Course[]>;
The generic structure remains the same while the data type changes.
5. Function Types
You can explicitly define the type of a function.
1type Add = ( 2 a: number, 3 b: number 4) => number;
Then:
1const add: Add = ( 2 a, 3 b 4) => { 5 return a + b; 6};
TypeScript knows:
1a → number 2b → number 3return → number
Function Types in Next.js
Function types are useful for:
- Event handlers
- Service functions
- Callbacks
- API functions
- Utility functions
For example:
1type GetCourse = ( 2 slug: string 3) => Promise<Course>;
Then:
1const getCourse: GetCourse = 2 async (slug) => { 3 // Fetch course 4 };
6. API Response Types
API responses should ideally have explicit types.
Suppose your API returns:
1{ 2 "id": "101", 3 "title": "Next.js", 4 "price": 499 5}
Create:
1type Course = { 2 id: string; 3 title: string; 4 price: number; 5};
Then:
1async function getCourse( 2 slug: string 3): Promise<Course> { 4 const response = await fetch( 5 `/api/courses/${slug}` 6 ); 7 8 if (!response.ok) { 9 throw new Error( 10 "Failed to fetch course" 11 ); 12 } 13 14 return response.json(); 15}
Now the function promises:
1Promise<Course>
7. Generic API Client
A reusable API client can use generics.
1async function apiFetch<T>( 2 url: string 3): Promise<T> { 4 const response = await fetch(url); 5 6 if (!response.ok) { 7 throw new Error( 8 "API request failed" 9 ); 10 } 11 12 return response.json(); 13}
Now:
1const course = 2 await apiFetch<Course>( 3 "/api/courses/nextjs" 4 );
And:
1const courses = 2 await apiFetch<Course[]>( 3 "/api/courses" 4 );
The same function works for both:
1apiFetch<Course>() 2 ↓ 3Course 4 5apiFetch<Course[]>() 6 ↓ 7Course[]
8. Component Props
Props should be typed.
1type ButtonProps = { 2 title: string; 3 disabled?: boolean; 4}; 5 6export function Button({ 7 title, 8 disabled = false, 9}: ButtonProps) { 10 return ( 11 <button disabled={disabled}> 12 {title} 13 </button> 14 ); 15}
Usage:
1<Button 2 title="Save" 3 disabled={false} 4/>
TypeScript prevents incorrect values such as:
1<Button 2 title={123} 3/>
because title expects a string.
9. Optional Properties
A property can be optional using ?.
1type User = { 2 id: string; 3 name: string; 4 avatar?: string; 5};
Now both are valid:
1const user1: User = { 2 id: "1", 3 name: "Ankit", 4};
and:
1const user2: User = { 2 id: "2", 3 name: "Rahul", 4 avatar: "/avatar.png", 5};
The avatar property is optional.
Optional Props in React Components
1type CardProps = { 2 title: string; 3 description?: string; 4}; 5 6export function Card({ 7 title, 8 description, 9}: CardProps) { 10 return ( 11 <article> 12 <h2>{title}</h2> 13 14 {description && ( 15 <p>{description}</p> 16 )} 17 </article> 18 ); 19}
Now:
1<Card title="Next.js" />
is valid.
And:
1<Card 2 title="Next.js" 3 description="Learn Next.js" 4/>
is also valid.
10. Union Types
A union allows a value to have one of several types.
1type Status = 2 | "loading" 3 | "success" 4 | "error";
Now:
1let status: Status; 2 3status = "loading"; 4status = "success"; 5status = "error";
But:
1status = "completed";
is invalid.
Union Types for API States
This is very useful for UI state:
1type RequestState = 2 | "idle" 3 | "loading" 4 | "success" 5 | "error";
A component can use:
1const [status, setStatus] = 2 useState<RequestState>("idle");
Now the state cannot accidentally become an unrelated value.
Discriminated Unions
For more advanced API states:
1type Result<T> = 2 | { 3 success: true; 4 data: T; 5 } 6 | { 7 success: false; 8 error: string; 9 };
Now:
1function handleResult( 2 result: Result<Course> 3) { 4 if (result.success) { 5 console.log( 6 result.data.title 7 ); 8 } else { 9 console.error( 10 result.error 11 ); 12 } 13}
TypeScript understands which properties are available based on success.
11. Utility Types
TypeScript provides built-in utility types for transforming existing types.
Common utilities include:
1Partial 2Required 3Pick 4Omit 5Record 6Readonly 7ReturnType 8Parameters 9Awaited
These are extremely useful in Next.js applications.
Partial
Suppose:
1type Course = { 2 id: string; 3 title: string; 4 price: number; 5};
For an update operation, you may not need every field.
1type UpdateCourse = 2 Partial<Course>;
Now all properties are optional:
1const update: UpdateCourse = { 2 title: "Advanced Next.js", 3};
This is useful for PATCH APIs.
Pick
Pick selects specific properties.
1type CoursePreview = 2 Pick<Course, "id" | "title">;
Now:
1const preview: CoursePreview = { 2 id: "101", 3 title: "Next.js", 4};
The preview does not need:
1price
or other course fields.
Omit
Omit removes properties.
1type PublicCourse = 2 Omit<Course, "price">;
This can be useful when creating a public representation of a type.
Record
Record is useful for dictionaries/maps.
1type CourseMap = 2 Record<string, Course>;
Example:
1const courses: CourseMap = { 2 nextjs: { 3 id: "1", 4 title: "Next.js", 5 price: 499, 6 }, 7};
Readonly
Readonly prevents modification through that type.
1type ReadonlyCourse = 2 Readonly<Course>;
This is useful when a function should only consume data.
ReturnType
You can derive the return type of a function.
1function getCourse() { 2 return { 3 id: "1", 4 title: "Next.js", 5 }; 6} 7 8type Course = 9 ReturnType<typeof getCourse>;
Now TypeScript derives the type automatically.
Awaited
For asynchronous functions:
1async function getCourse() { 2 return { 3 id: "1", 4 title: "Next.js", 5 }; 6}
You can extract the resolved value:
1type Course = 2 Awaited< 3 ReturnType<typeof getCourse> 4 >;
This is useful for complex service architectures.
12. Async Function Types
An asynchronous function usually returns a Promise.
1type GetCourse = ( 2 slug: string 3) => Promise<Course>;
Implementation:
1const getCourse: GetCourse = 2 async (slug) => { 3 const response = 4 await fetch( 5 `/api/courses/${slug}` 6 ); 7 8 return response.json(); 9 };
The important concept is:
1Normal function 2↓ 3Course 4 5Async function 6↓ 7Promise<Course>
Async Service Functions
A typical Next.js service:
1type Course = { 2 id: string; 3 title: string; 4 description: string; 5}; 6 7export async function getCourse( 8 slug: string 9): Promise<Course> { 10 const response = 11 await fetch( 12 `/api/courses/${slug}` 13 ); 14 15 if (!response.ok) { 16 throw new Error( 17 "Course request failed" 18 ); 19 } 20 21 return response.json(); 22}
Now every caller knows:
1getCourse() 2 ↓ 3Promise<Course>
13. Generic Service Function
Now combine generics and async functions.
1async function apiFetch<T>( 2 url: string 3): Promise<T> { 4 const response = 5 await fetch(url); 6 7 if (!response.ok) { 8 throw new Error( 9 "API request failed" 10 ); 11 } 12 13 return response.json(); 14}
Use it:
1const course = 2 await apiFetch<Course>( 3 "/api/courses/nextjs" 4 );
Or:
1const courses = 2 await apiFetch<Course[]>( 3 "/api/courses" 4 );
This gives you a reusable typed API layer.
14. Generic API Response
A production API may return:
1{ 2 "success": true, 3 "data": { 4 "id": "101", 5 "title": "Next.js" 6 } 7}
Define:
1type ApiResponse<T> = { 2 success: boolean; 3 data: T; 4};
Then:
1type CourseResponse = 2 ApiResponse<Course>;
And:
1type CoursesResponse = 2 ApiResponse<Course[]>;
Now:
1async function apiFetch<T>( 2 url: string 3): Promise<ApiResponse<T>> { 4 const response = 5 await fetch(url); 6 7 if (!response.ok) { 8 throw new Error( 9 "API request failed" 10 ); 11 } 12 13 return response.json(); 14}
Usage:
1const response = 2 await apiFetch<Course>( 3 "/api/courses/nextjs" 4 ); 5 6console.log( 7 response.data.title 8);
15. Generic Function Schema
The basic generic function pattern is:
1function functionName<T>( 2 value: T 3): T { 4 return value; 5}
For an asynchronous API function:
1async function apiFetch<T>( 2 url: string 3): Promise<T> { 4 const response = 5 await fetch(url); 6 7 return response.json(); 8}
The important structure is:
1<T> 2 ↓ 3Generic input type 4 5Promise<T> 6 ↓ 7Generic async return type
16. Advanced Function Design
A reusable API client can be designed like this:
1async function apiFetch< 2 T, 3 TError = string 4>( 5 url: string, 6 options?: RequestInit 7): Promise<T> { 8 const response = 9 await fetch( 10 url, 11 options 12 ); 13 14 if (!response.ok) { 15 throw new Error( 16 `Request failed: ${response.status}` 17 ); 18 } 19 20 return response.json(); 21}
Now you can specify the expected result:
1const course = 2 await apiFetch<Course>( 3 "/api/courses/nextjs" 4 );
Or:
1const courses = 2 await apiFetch<Course[]>( 3 "/api/courses" 4 );
The function remains reusable.
17. Generic Functions With Arrays
A generic function can also work with arrays.
1function first<T>( 2 items: T[] 3): T | undefined { 4 return items[0]; 5}
Usage:
1const course = 2 first<Course>(courses);
TypeScript understands that:
1Course[] 2 ↓ 3first() 4 ↓ 5Course | undefined
18. Generic API Client With HTTP Methods
A more realistic API client:
1async function apiFetch<T>( 2 url: string, 3 options?: RequestInit 4): Promise<T> { 5 const response = 6 await fetch(url, { 7 ...options, 8 headers: { 9 "Content-Type": 10 "application/json", 11 ...options?.headers, 12 }, 13 }); 14 15 if (!response.ok) { 16 throw new Error( 17 `HTTP ${response.status}` 18 ); 19 } 20 21 return response.json(); 22}
Now:
GET
1const course = 2 await apiFetch<Course>( 3 "/api/courses/nextjs" 4 );
POST
1const course = 2 await apiFetch<Course>( 3 "/api/courses", 4 { 5 method: "POST", 6 body: JSON.stringify({ 7 title: "Next.js", 8 }), 9 } 10 );
PATCH
1const course = 2 await apiFetch<Course>( 3 "/api/courses/nextjs", 4 { 5 method: "PATCH", 6 body: JSON.stringify({ 7 title: "Advanced Next.js", 8 }), 9 } 10 );
The same generic function supports multiple API operations.
19. Typing Route Parameters
Next.js dynamic routes can also be typed.
For:
1app/courses/[slug]/page.tsx
you can write:
1type PageProps = { 2 params: Promise<{ 3 slug: string; 4 }>; 5}; 6 7export default async function CoursePage({ 8 params, 9}: PageProps) { 10 const { slug } = await params; 11 12 return ( 13 <h1> 14 Course: {slug} 15 </h1> 16 ); 17}
Now the route structure and TypeScript structure agree:
1[courses] 2 ↓ 3[slug] 4 ↓ 5string
20. Typing Search Parameters
Search parameters can also be typed.
1type PageProps = { 2 searchParams: Promise<{ 3 page?: string; 4 category?: string; 5 }>; 6}; 7 8export default async function CoursesPage({ 9 searchParams, 10}: PageProps) { 11 const params = 12 await searchParams; 13 14 const page = 15 params.page ?? "1"; 16 17 const category = 18 params.category; 19 20 return ( 21 <div> 22 Page: {page} 23 24 {category && ( 25 <p> 26 Category: {category} 27 </p> 28 )} 29 </div> 30 ); 31}
Optional properties are appropriate because query parameters may not exist.
21. Typing Children
React components often receive children.
1import type { ReactNode } from "react"; 2 3type LayoutProps = { 4 children: ReactNode; 5}; 6 7export function Layout({ 8 children, 9}: LayoutProps) { 10 return ( 11 <main> 12 {children} 13 </main> 14 ); 15}
Now the component accepts React content safely.
22. Typing Event Handlers
Client Components often use event handlers.
1"use client"; 2 3import type { 4 ChangeEvent, 5} from "react"; 6 7export function SearchBox() { 8 function handleChange( 9 event: ChangeEvent<HTMLInputElement> 10 ) { 11 console.log( 12 event.target.value 13 ); 14 } 15 16 return ( 17 <input 18 onChange={handleChange} 19 /> 20 ); 21}
TypeScript knows:
1event 2 ↓ 3ChangeEvent<HTMLInputElement> 4 ↓ 5event.target 6 ↓ 7HTMLInputElement
23. Typing Form Submission
1"use client"; 2 3import type { 4 FormEvent, 5} from "react"; 6 7export function CourseForm() { 8 function handleSubmit( 9 event: FormEvent<HTMLFormElement> 10 ) { 11 event.preventDefault(); 12 13 const form = 14 new FormData( 15 event.currentTarget 16 ); 17 18 console.log( 19 form.get("title") 20 ); 21 } 22 23 return ( 24 <form onSubmit={handleSubmit}> 25 <input name="title" /> 26 27 <button type="submit"> 28 Create 29 </button> 30 </form> 31 ); 32}
This gives you type safety for browser events.
24. Avoid any
One of the most important TypeScript rules is:
Avoid using
anyunless there is a very specific reason.
Bad:
1function getCourse( 2 data: any 3) { 4 return data.title; 5}
Now TypeScript cannot protect you.
Better:
1type Course = { 2 id: string; 3 title: string; 4}; 5 6function getCourse( 7 data: Course 8) { 9 return data.title; 10}
Now invalid data is much easier to detect.
25. unknown vs any
When you genuinely don't know the type of external data, unknown is safer.
1function processData( 2 data: unknown 3) { 4 // Validate before using 5}
For example:
1function isCourse( 2 value: unknown 3): value is Course { 4 if ( 5 typeof value !== "object" || 6 value === null 7 ) { 8 return false; 9 } 10 11 return ( 12 "id" in value && 13 "title" in value 14 ); 15}
Then:
1if (isCourse(data)) { 2 console.log(data.title); 3}
This is much safer than blindly using any.
26. TypeScript Does Not Validate Runtime Data
This is extremely important.
TypeScript types disappear at runtime.
For example:
1type Course = { 2 title: string; 3};
does not automatically validate API data.
An external API could still return:
1{ 2 "title": 123 3}
Therefore:
1TypeScript 2 ↓ 3Compile-time safety
while runtime validation requires tools or explicit checks:
1API Response 2 ↓ 3Runtime Validation 4 ↓ 5TypeScript-safe Data
Libraries such as Zod are commonly used for this purpose.
27. TypeScript + Zod
You can define a runtime schema:
1import { z } from "zod"; 2 3const courseSchema = z.object({ 4 id: z.string(), 5 title: z.string(), 6 price: z.number(), 7});
Then validate API data:
1const data = 2 await response.json(); 3 4const course = 5 courseSchema.parse(data);
Now you have:
1External Data 2 ↓ 3Zod Validation 4 ↓ 5Validated Course 6 ↓ 7Application
This creates a stronger boundary around external data.
28. Complete Typed Service Architecture
Let's combine everything.
Course type
1export type Course = { 2 id: string; 3 slug: string; 4 title: string; 5 description: string; 6 price: number; 7};
Generic API client
1export async function apiFetch<T>( 2 url: string, 3 options?: RequestInit 4): Promise<T> { 5 const response = 6 await fetch(url, options); 7 8 if (!response.ok) { 9 throw new Error( 10 `API request failed: ${response.status}` 11 ); 12 } 13 14 return response.json(); 15}
Course service
1import type { Course } from "@/types/course"; 2import { apiFetch } from "@/lib/api"; 3 4export function getCourse( 5 slug: string 6): Promise<Course> { 7 return apiFetch<Course>( 8 `/api/courses/${slug}` 9 ); 10}
Server Component
1import { getCourse } from "@/services/course.service"; 2 3export default async function CoursePage({ 4 params, 5}: { 6 params: Promise<{ 7 slug: string; 8 }>; 9}) { 10 const { slug } = await params; 11 12 const course = 13 await getCourse(slug); 14 15 return ( 16 <article> 17 <h1>{course.title}</h1> 18 19 <p> 20 {course.description} 21 </p> 22 23 <strong> 24 ₹{course.price} 25 </strong> 26 </article> 27 ); 28}
The complete type-safe architecture is:
1Course Type 2 ↓ 3Generic API Client 4 ↓ 5Course Service 6 ↓ 7Server Component 8 ↓ 9UI
29. Advanced Function Design
The function design from this module can be summarized as:
1async function apiFetch< 2 T 3>( 4 url: string, 5 options?: RequestInit 6): Promise<T> { 7 const response = 8 await fetch( 9 url, 10 options 11 ); 12 13 if (!response.ok) { 14 throw new Error( 15 `Request failed: ${response.status}` 16 ); 17 } 18 19 return response.json(); 20}
Usage:
1const course = 2 await apiFetch<Course>( 3 "/api/courses/nextjs" 4 );
1const courses = 2 await apiFetch<Course[]>( 3 "/api/courses" 4 );
The important pattern is:
1<T> 2 ↓ 3Caller specifies expected type 4 5Promise<T> 6 ↓ 7Async function returns that type
This pattern is extremely common in professional TypeScript codebases.
30. TypeScript Mental Model
Think about TypeScript as a contract system.
1 TypeScript 2 │ 3 ┌──────────┼──────────┐ 4 ▼ ▼ ▼ 5 Props API Functions 6 │ │ │ 7 ▼ ▼ ▼ 8 Types Types Types 9 │ │ │ 10 └──────────┼──────────┘ 11 ▼ 12 Safer Application
When data moves through your application:
1API 2 ↓ 3Course 4 ↓ 5Service 6 ↓ 7Page 8 ↓ 9Component 10 ↓ 11Props
the types document what each layer expects.
Common TypeScript Mistakes in Next.js
Mistake 1 — Using any everywhere
1const data: any = ...
This removes much of TypeScript's value.
Mistake 2 — Assuming API data is automatically safe
1const course: Course = 2 await response.json();
The annotation does not validate the runtime response.
Use runtime validation for untrusted external data.
Mistake 3 — Huge duplicated types
Avoid redefining the same structure in many files.
Instead:
1types/ 2├── course.ts 3├── user.ts 4├── project.ts 5└── api.ts
Reuse shared types.
Mistake 4 — Overcomplicated generics
Generics are powerful, but don't introduce them when a simple type is enough.
Simple:
1function getCourse(): 2 Promise<Course>
Use generics when you genuinely need reusable behavior:
1function apiFetch<T>(): 2 Promise<T>
Mistake 5 — Confusing compile-time and runtime validation
Remember:
1TypeScript 2 ↓ 3Compile time 4 5Zod / validation 6 ↓ 7Runtime
For external APIs, runtime validation may be necessary.
Final TypeScript Architecture
A strong Next.js TypeScript application can follow:
1 TypeScript 2 │ 3 ┌─────────────┼─────────────┐ 4 ▼ ▼ ▼ 5 Components Services APIs 6 │ │ │ 7 Props Functions Responses 8 │ │ │ 9 └─────────────┼─────────────┘ 10 ▼ 11 Shared Types 12 │ 13 ▼ 14 Safer Data Flow
For a real course application:
1PostgreSQL 2 ↓ 3Django / Node.js 4 ↓ 5API Response 6 ↓ 7Course Type 8 ↓ 9Generic API Client 10 ↓ 11Course Service 12 ↓ 13Next.js Server Component 14 ↓ 15Typed Component Props 16 ↓ 17UI
The key idea is:
Use TypeScript to make the contracts between your database, APIs, services, pages, and components explicit.
Once you understand types, interfaces, generics, function types, API response types, utility types, and async function types, you can build much larger Next.js applications with significantly better maintainability and developer safety.
Module 17 Learning Checklist
After completing this module, you should understand:
- Type aliases
- Interfaces
- Interface inheritance
- Generics
- Function types
- Async function types
- Component props
- Optional properties
- Union types
- Discriminated unions
- API response types
- Generic API clients
- Utility types
PartialPickOmitRecordReadonlyReturnTypeAwaited- Typed route parameters
- Typed search parameters
- Typed React events
anyvsunknown- Runtime validation
- TypeScript with Zod
- Type-safe service architecture