Back to Blog
Next.js7 min read

TypeScript with Next.js: Achieving Full Type Safety

Advanced TypeScript patterns for Next.js applications that catch errors before runtime.

February 26, 2024
#TypeScript#Next.js#DX

Setting Up TypeScript


Next.js has built-in TypeScript support:


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

Essential Type Patterns


API Response Types


// Define the expected shape

interface ApiResponse<T> {

data: T;

success: boolean;

error?: string;

timestamp: number;

}


interface User {

id: string;

name: string;

email: string;

}


// Type the response

async function fetchUser(id: string): Promise<ApiResponse<User>> {

const res = await fetch(/api/users/${id});

return res.json();

}

Form Types with Zod


import { z } from 'zod';


const SignupSchema = z.object({

name: z.string().min(2, 'Name too short'),

email: z.string().email('Invalid email'),

password: z.string().min(8, 'Password must be 8+ chars'),

confirmPassword: z.string(),

}).refine(data => data.password === data.confirmPassword, {

message: "Passwords don't match",

path: ['confirmPassword'],

});


type SignupForm = z.infer<typeof SignupSchema>;

Component Props with Generics


interface ListProps<T> {

items: T[];

renderItem: (item: T) => React.ReactNode;

keyExtractor: (item: T) => string;

}


function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {

return (

<ul>

{items.map(item => (

<li key={keyExtractor(item)}>{renderItem(item)}</li>

))}

</ul>

);

}


// Usage

<List

items={users}

renderItem={user => <span>{user.name}</span>}

keyExtractor={user => user.id}

/>

Strict Mode Benefits


Enable strict mode in tsconfig.json:


{

"compilerOptions": {

"strict": true,

"noUncheckedIndexedAccess": true,

"noImplicitReturns": true,

"noFallthroughCasesInSwitch": true

}

}

Conclusion


TypeScript catches bugs at compile time, reducing runtime errors. Invest in proper typing - your future self will thank you.

npx create-next-app@latest my-app --typescript
// Define the expected shape interface ApiResponse<T> { data: T; success: boolean; error?: string; timestamp: number; } interface User { id: string; name: string; email: string; } // Type the response async function fetchUser(id: string): Promise<ApiResponse<User>> { const res = await fetch(`/api/users/${id}`); return res.json(); }
import { z } from 'zod'; const SignupSchema = z.object({ name: z.string().min(2, 'Name too short'), email: z.string().email('Invalid email'), password: z.string().min(8, 'Password must be 8+ chars'), confirmPassword: z.string(), }).refine(data => data.password === data.confirmPassword, { message: "Passwords don't match", path: ['confirmPassword'], }); type SignupForm = z.infer<typeof SignupSchema>;
interface ListProps<T> { items: T[]; renderItem: (item: T) => React.ReactNode; keyExtractor: (item: T) => string; } function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) { return ( <ul> {items.map(item => ( <li key={keyExtractor(item)}>{renderItem(item)}</li> ))} </ul> ); } // Usage <List items={users} renderItem={user => <span>{user.name}</span>} keyExtractor={user => user.id} />
{ "compilerOptions": { "strict": true, "noUncheckedIndexedAccess": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true } }

Need Help with Your Project?

Our team can help you implement these patterns in your application.

Get in Touch