Skip to main content

TypeScript Integration Example

This example demonstrates how to integrate TypeScript in a Next.js project following the Folder-Zen structure.

Project Setup

1. TypeScript Configuration

First, let's set up the TypeScript configuration in tsconfig.json:

tsconfig.json
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"baseUrl": ".",
"paths": {
"@c/*": ["src/components/*"],
"@v/*": ["src/views/*"],
"@s/*": ["src/services/*"],
"@l/*": ["src/libs/*"],
"@m/*": ["src/modules/*"],
"@b/*": ["src/base/*"],
"@/*": ["src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}

2. Next.js Configuration

Configure Next.js to work with TypeScript:

next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
}

module.exports = nextConfig

Type Organization Examples

1. Global Types

Create a global types directory for types used across the application:

src/types/index.ts
// Re-export all types
export * from './api';
export * from './common';
export * from './theme';
src/types/common.ts
export interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'user';
}

export interface Pagination {
page: number;
limit: number;
total: number;
totalPages: number;
}
src/types/api.ts
import { Pagination } from './common';

export interface ApiResponse<T> {
data: T;
status: number;
message: string;
}

export interface PaginatedResponse<T> extends ApiResponse<T[]> {
pagination: Pagination;
}

2. Component Types

Create types for components:

src/components/ui/Button/Button.types.ts
import { ReactNode } from 'react';

export interface ButtonProps {
variant?: 'primary' | 'secondary' | 'outline';
size?: 'small' | 'medium' | 'large';
disabled?: boolean;
onClick?: () => void;
children: ReactNode;
className?: string;
}
src/components/ui/Button/Button.tsx
import React from 'react';
import { ButtonProps } from './Button.types';
import styles from './Button.module.css';

const Button: React.FC<ButtonProps> = ({
variant = 'primary',
size = 'medium',
disabled = false,
onClick,
children,
className = '',
}) => {
const buttonClass = `${styles.button} ${styles[variant]} ${styles[size]} ${className}`;

return (
<button
className={buttonClass}
disabled={disabled}
onClick={onClick}
type="button"
>
{children}
</button>
);
};

export default Button;

3. Module Types

Create types for modules:

src/modules/auth/types/index.ts
export * from './user';
export * from './auth';
src/modules/auth/types/user.ts
export interface User {
id: string;
name: string;
email: string;
role: 'admin' | 'user';
}

export interface UserProfile extends User {
avatar?: string;
bio?: string;
location?: string;
}
src/modules/auth/types/auth.ts
export interface LoginCredentials {
email: string;
password: string;
}

export interface AuthState {
isAuthenticated: boolean;
user: User | null;
loading: boolean;
error: string | null;
}

4. Service Types

Create types for services:

src/services/api/types.ts
import { ApiResponse, PaginatedResponse } from '@/types';
import { User } from '@m/auth/types';

export interface UserApiResponse extends ApiResponse<User> {}
export interface UsersApiResponse extends PaginatedResponse<User> {}

Practical Usage Examples

1. Using Path Aliases

src/modules/auth/components/LoginForm/LoginForm.tsx
import { useState } from 'react';
import { LoginCredentials } from '@m/auth/types';
import Button from '@c/ui/Button';
import { authService } from '@s/auth';
import styles from './LoginForm.module.css';

interface LoginFormProps {
onSuccess: () => void;
onError: (error: string) => void;
}

export const LoginForm: React.FC<LoginFormProps> = ({ onSuccess, onError }) => {
const [credentials, setCredentials] = useState<LoginCredentials>({
email: '',
password: '',
});
const [loading, setLoading] = useState(false);

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setCredentials((prev) => ({ ...prev, [name]: value }));
};

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);

try {
await authService.login(credentials);
onSuccess();
} catch (error) {
onError(error instanceof Error ? error.message : 'Login failed');
} finally {
setLoading(false);
}
};

return (
<form className={styles.form} onSubmit={handleSubmit}>
<div className={styles.formGroup}>
<label htmlFor="email" className={styles.label}>
Email
</label>
<input
id="email"
type="email"
name="email"
value={credentials.email}
onChange={handleChange}
className={styles.input}
required
/>
</div>

<div className={styles.formGroup}>
<label htmlFor="password" className={styles.label}>
Password
</label>
<input
id="password"
type="password"
name="password"
value={credentials.password}
onChange={handleChange}
className={styles.input}
required
/>
</div>

<Button
variant="primary"
size="medium"
disabled={loading}
className={styles.submitButton}
>
{loading ? 'Logging in...' : 'Log In'}
</Button>
</form>
);
};

export default LoginForm;

2. Using TypeScript Utility Types

src/services/auth/authService.ts
import { ApiResponse } from '@/types';
import { LoginCredentials, User } from '@m/auth/types';
import { apiService } from '@s/api';

// Using TypeScript utility types
type LoginResponse = ApiResponse<{ user: User; token: string }>;
type RegisterParams = Omit<User, 'id' | 'role'> & { password: string };

export const authService = {
async login(credentials: LoginCredentials): Promise<LoginResponse> {
return apiService.post<LoginResponse>('/auth/login', credentials);
},

async register(params: RegisterParams): Promise<LoginResponse> {
return apiService.post<LoginResponse>('/auth/register', params);
},

async logout(): Promise<void> {
return apiService.post<void>('/auth/logout');
},

async getProfile(): Promise<ApiResponse<User>> {
return apiService.get<User>('/auth/profile');
}
};

3. Using Type Guards

src/libs/utils/typeGuards.ts
import { User } from '@m/auth/types';

export function isUser(obj: unknown): obj is User {
return (
typeof obj === 'object' &&
obj !== null &&
'id' in obj &&
'name' in obj &&
'email' in obj &&
'role' in obj
);
}

export function isApiError(obj: unknown): obj is { message: string; status: number } {
return (
typeof obj === 'object' &&
obj !== null &&
'message' in obj &&
'status' in obj
);
}
src/modules/auth/hooks/useAuth.ts
import { useState, useEffect } from 'react';
import { User } from '@m/auth/types';
import { authService } from '@s/auth';
import { isUser, isApiError } from '@l/utils/typeGuards';

export function useAuth() {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);

useEffect(() => {
const fetchUser = async () => {
try {
const response = await authService.getProfile();

if (isUser(response.data)) {
setUser(response.data);
} else {
setError('Invalid user data');
}
} catch (err) {
if (isApiError(err)) {
setError(err.message);
} else {
setError('Failed to fetch user profile');
}
} finally {
setLoading(false);
}
};

fetchUser();
}, []);

return { user, loading, error };
}

Conclusion

This example demonstrates how to integrate TypeScript in a Next.js project following the Folder-Zen structure. By organizing types according to the layer they belong to and using TypeScript features like utility types and type guards, you can create a type-safe and maintainable application.