Writing great TypeScript code goes beyond just adding types. It involves following conventions, organizing your code well, configuring TypeScript properly, and adopting patterns that make your code maintainable and scalable. Let's master TypeScript best practices!
Enable Strict Mode
Strict mode is essential for catching bugs early. Always enable it in your tsconfig.json for maximum type safety.
{
"compilerOptions": {
// Enable all strict type checking options
"strict": true,
// Individual strict options (included in strict: true)
"noImplicitAny": true, // Error on 'any' inference
"strictNullChecks": true, // null and undefined are not in every type
"strictFunctionTypes": true, // Strict function type checking
"strictBindCallApply": true, // Strict bind/call/apply
"strictPropertyInitialization": true, // Class properties must be initialized
"noImplicitThis": true, // Error on 'this' with implied 'any'
"alwaysStrict": true, // Emit "use strict"
// Additional strictness
"noUnusedLocals": true, // Error on unused local variables
"noUnusedParameters": true, // Error on unused parameters
"noImplicitReturns": true, // Error on missing return statements
"noFallthroughCasesInSwitch": true, // Error on switch fallthrough
"noUncheckedIndexedAccess": true, // Add undefined to index signatures
// Module resolution
"moduleResolution": "node",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
// Output options
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020", "DOM"],
"outDir": "./dist",
"rootDir": "./src",
// Source maps and debugging
"sourceMap": true,
"declaration": true,
"declarationMap": true,
// Import/export
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}// With strict mode enabled
// ✅ noImplicitAny catches this
function greet(name: string) { // Must specify type
return `Hello, ${name}`;
}
// ✅ strictNullChecks prevents this
function getLength(text: string | null): number {
// return text.length; // ❌ Error: Object is possibly 'null'
if (text === null) {
return 0;
}
return text.length; // ✅ Safe
}
// ✅ strictPropertyInitialization requires initialization
class User {
name: string; // ❌ Error: Property has no initializer
email: string;
constructor(name: string, email: string) {
this.name = name;
this.email = email; // ✅ Now initialized
}
}
// Or use definite assignment assertion
class Product {
id!: number; // ! tells TypeScript "I'll initialize this"
constructor() {
this.initialize();
}
private initialize(): void {
this.id = Date.now();
}
}
// ✅ noUnusedLocals catches unused variables
function calculate(a: number, b: number): number {
const unused = 10; // ❌ Error: unused is declared but never used
return a + b;
}
// ✅ noImplicitReturns ensures all code paths return
function getDiscount(amount: number): number {
if (amount > 100000) {
return 0.15;
} else if (amount > 50000) {
return 0.10;
}
// ❌ Error: Not all code paths return a value
return 0; // ✅ Fix
}Naming Conventions
// ✅ Interfaces and Types - PascalCase
interface User {
id: number;
name: string;
}
type UserRole = 'admin' | 'editor' | 'viewer';
// ✅ Classes - PascalCase
class UserService {
private users: User[] = [];
addUser(user: User): void {
this.users.push(user);
}
}
// ✅ Functions and variables - camelCase
function calculateTotal(items: number[]): number {
return items.reduce((sum, item) => sum + item, 0);
}
const totalAmount = calculateTotal([100, 200, 300]);
const userCount = 10;
// ✅ Constants - SCREAMING_SNAKE_CASE
const API_URL = 'https://api.example.com';
const MAX_RETRY_ATTEMPTS = 3;
const DEFAULT_TIMEOUT = 30000;
// ✅ Enums - PascalCase for enum, PascalCase for members
enum OrderStatus {
Pending = 'PENDING',
Processing = 'PROCESSING',
Shipped = 'SHIPPED',
Delivered = 'DELIVERED',
Cancelled = 'CANCELLED'
}
// ✅ Private class members - prefix with underscore (optional)
class BankAccount {
private _balance: number;
constructor(initialBalance: number) {
this._balance = initialBalance;
}
get balance(): number {
return this._balance;
}
private _validateAmount(amount: number): boolean {
return amount > 0;
}
}
// ✅ Generic type parameters - single uppercase letter or PascalCase
function identity<T>(value: T): T {
return value;
}
function mapValues<TInput, TOutput>(
items: TInput[],
mapper: (item: TInput) => TOutput
): TOutput[] {
return items.map(mapper);
}
// ✅ Boolean variables - prefix with is/has/should
const isActive = true;
const hasPermission = false;
const shouldRetry = true;
function isValidEmail(email: string): boolean {
return email.includes('@');
}
// ✅ Event handlers - prefix with 'handle' or 'on'
function handleSubmit(event: Event): void {
event.preventDefault();
}
const onClick = (event: MouseEvent) => {
console.log('Clicked');
};
// ✅ Async functions - descriptive names
async function fetchUserData(userId: number): Promise<User> {
const response = await fetch(`/api/users/${userId}`);
return response.json();
}
async function saveProductToDatabase(product: Product): Promise<void> {
// Save logic
}
interface Product {
id: number;
name: string;
}Type Safety Best Practices
// ✅ Avoid 'any' - use specific types
// ❌ Bad
function processData(data: any): any {
return data;
}
// ✅ Good
interface ApiResponse {
success: boolean;
data: unknown;
}
function processData(response: ApiResponse): void {
if (response.success) {
// Validate data before using
}
}
// ✅ Use 'unknown' instead of 'any' when type is truly unknown
function parseJSON(json: string): unknown {
return JSON.parse(json);
}
function safelyUseData(data: unknown): void {
if (typeof data === 'string') {
console.log(data.toUpperCase());
} else if (typeof data === 'number') {
console.log(data.toFixed(2));
}
}
// ✅ Use type guards for narrowing
function isUser(obj: unknown): obj is User {
return (
typeof obj === 'object' &&
obj !== null &&
'id' in obj &&
'name' in obj &&
'email' in obj
);
}
function processUser(data: unknown): void {
if (isUser(data)) {
console.log(data.name); // TypeScript knows this is User
}
}
// ✅ Use const assertions for literal types
const ROUTES = {
HOME: '/',
PRODUCTS: '/products',
ABOUT: '/about'
} as const;
type Route = typeof ROUTES[keyof typeof ROUTES];
// Route = "/" | "/products" | "/about"
// ✅ Prefer interfaces for object shapes
interface Product {
id: number;
name: string;
price: number;
}
// ✅ Use type aliases for unions, tuples, primitives
type Status = 'pending' | 'success' | 'error';
type Coordinates = [number, number];
type ID = string | number;
// ✅ Use readonly for immutability
interface Config {
readonly apiUrl: string;
readonly timeout: number;
}
const config: Config = {
apiUrl: 'https://api.example.com',
timeout: 5000
};
// config.apiUrl = 'other'; // ❌ Error: readonly
// ✅ Use ReadonlyArray for arrays that shouldn't be modified
function sum(numbers: readonly number[]): number {
// numbers.push(10); // ❌ Error: push doesn't exist on readonly array
return numbers.reduce((total, n) => total + n, 0);
}
// ✅ Use never for exhaustive checks
type Shape = Circle | Square | Triangle;
function getArea(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'square':
return shape.size ** 2;
case 'triangle':
return (shape.base * shape.height) / 2;
default:
const exhaustive: never = shape;
throw new Error(`Unhandled shape: ${exhaustive}`);
}
}
interface User {
id: number;
name: string;
email: string;
}
interface Circle {
kind: 'circle';
radius: number;
}
interface Square {
kind: 'square';
size: number;
}
interface Triangle {
kind: 'triangle';
base: number;
height: number;
}Code Organization
src/
├── types/ # Type definitions
│ ├── user.types.ts
│ ├── product.types.ts
│ ├── api.types.ts
│ └── index.ts # Re-export all types
│
├── interfaces/ # Shared interfaces
│ ├── IRepository.ts
│ ├── IService.ts
│ └── index.ts
│
├── models/ # Data models/classes
│ ├── User.ts
│ ├── Product.ts
│ └── index.ts
│
├── services/ # Business logic
│ ├── UserService.ts
│ ├── ProductService.ts
│ ├── AuthService.ts
│ └── index.ts
│
├── repositories/ # Data access
│ ├── UserRepository.ts
│ ├── ProductRepository.ts
│ └── index.ts
│
├── utils/ # Utility functions
│ ├── validators.ts
│ ├── formatters.ts
│ ├── helpers.ts
│ └── index.ts
│
├── constants/ # Application constants
│ ├── routes.ts
│ ├── config.ts
│ └── index.ts
│
├── api/ # API clients
│ ├── client.ts
│ ├── endpoints.ts
│ └── index.ts
│
└── index.ts # Main entry point// Keep related types together
export interface User {
id: number;
name: string;
email: string;
role: UserRole;
createdAt: Date;
updatedAt: Date;
}
export type UserRole = 'admin' | 'editor' | 'viewer';
export interface CreateUserDto {
name: string;
email: string;
password: string;
role?: UserRole;
}
export interface UpdateUserDto {
name?: string;
email?: string;
role?: UserRole;
}
export interface UserFilter {
role?: UserRole;
search?: string;
limit?: number;
offset?: number;
}// Barrel export for clean imports
export * from './user.types';
export * from './product.types';
export * from './api.types';
// Usage in other files:
import { User, Product, ApiResponse } from '@/types';Error Handling
// ✅ Custom error classes
class AppError extends Error {
constructor(
public statusCode: number,
message: string,
public isOperational: boolean = true
) {
super(message);
this.name = this.constructor.name;
Error.captureStackTrace(this, this.constructor);
}
}
class ValidationError extends AppError {
constructor(message: string) {
super(400, message);
}
}
class NotFoundError extends AppError {
constructor(resource: string, id: string | number) {
super(404, `${resource} with id ${id} not found`);
}
}
class UnauthorizedError extends AppError {
constructor(message: string = 'Unauthorized') {
super(401, message);
}
}
// ✅ Result type for explicit error handling
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
async function fetchUser(id: number): Promise<Result<User>> {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
if (response.status === 404) {
return {
success: false,
error: new NotFoundError('User', id)
};
}
return {
success: false,
error: new AppError(response.status, 'Failed to fetch user')
};
}
const user: User = await response.json();
return { success: true, data: user };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error : new Error(String(error))
};
}
}
// ✅ Using Result type
async function handleUserFetch(id: number): Promise<void> {
const result = await fetchUser(id);
if (result.success) {
console.log('User:', result.data.name);
} else {
if (result.error instanceof NotFoundError) {
console.log('User not found');
} else if (result.error instanceof UnauthorizedError) {
console.log('Please log in');
} else {
console.error('Error:', result.error.message);
}
}
}
// ✅ Global error handler
function handleError(error: unknown): void {
if (error instanceof AppError) {
console.error(`[${error.statusCode}] ${error.message}`);
if (!error.isOperational) {
// Critical error - should restart/alert
console.error('Critical error detected');
}
} else if (error instanceof Error) {
console.error(`Unexpected error: ${error.message}`);
} else {
console.error('Unknown error:', error);
}
}
// ✅ Validation with clear errors
function validateEmail(email: string): void {
if (!email) {
throw new ValidationError('Email is required');
}
if (!email.includes('@')) {
throw new ValidationError('Invalid email format');
}
if (email.length > 255) {
throw new ValidationError('Email is too long (max 255 characters)');
}
}
interface User {
id: number;
name: string;
email: string;
}Function Best Practices
// ✅ Single Responsibility Principle
// ❌ Bad: Function does too much
function processOrder(order: Order): void {
// Validate
if (!order.items.length) throw new Error('No items');
// Calculate
const total = order.items.reduce((sum, item) => sum + item.price, 0);
// Save to database
database.save(order);
// Send email
sendEmail(order.userEmail, 'Order confirmation');
// Update inventory
updateInventory(order.items);
}
// ✅ Good: Separated concerns
function validateOrder(order: Order): void {
if (!order.items.length) {
throw new ValidationError('Order must have at least one item');
}
}
function calculateOrderTotal(items: OrderItem[]): number {
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
}
async function saveOrder(order: Order): Promise<void> {
await database.save(order);
}
async function sendOrderConfirmation(order: Order): Promise<void> {
await emailService.send({
to: order.userEmail,
subject: 'Order Confirmation',
template: 'order-confirmation',
data: { order }
});
}
async function updateInventory(items: OrderItem[]): Promise<void> {
for (const item of items) {
await inventoryService.decrementStock(item.productId, item.quantity);
}
}
// ✅ Now orchestrate
async function processOrder(order: Order): Promise<void> {
validateOrder(order);
const total = calculateOrderTotal(order.items);
const orderWithTotal = { ...order, total };
await saveOrder(orderWithTotal);
await sendOrderConfirmation(orderWithTotal);
await updateInventory(order.items);
}
// ✅ Keep functions small and focused
// ❌ Bad
function createUser(data: any): any {
const user = {
id: generateId(),
name: data.name?.trim() || '',
email: data.email?.toLowerCase() || '',
role: data.role || 'user',
createdAt: new Date()
};
if (!user.name) throw new Error('Name required');
if (!user.email.includes('@')) throw new Error('Invalid email');
database.save(user);
sendWelcomeEmail(user.email);
return user;
}
// ✅ Good
interface CreateUserDto {
name: string;
email: string;
role?: UserRole;
}
function validateUserData(data: CreateUserDto): void {
if (!data.name.trim()) {
throw new ValidationError('Name is required');
}
if (!data.email.includes('@')) {
throw new ValidationError('Invalid email format');
}
}
function normalizeUserData(data: CreateUserDto): User {
return {
id: generateId(),
name: data.name.trim(),
email: data.email.toLowerCase(),
role: data.role || 'user',
createdAt: new Date()
};
}
async function createUser(data: CreateUserDto): Promise<User> {
validateUserData(data);
const user = normalizeUserData(data);
await database.save(user);
await sendWelcomeEmail(user.email);
return user;
}
// ✅ Use descriptive parameter names
// ❌ Bad
function calc(a: number, b: number, c: boolean): number {
return c ? a + b : a - b;
}
// ✅ Good
function calculateTotal(
subtotal: number,
tax: number,
includeTax: boolean
): number {
return includeTax ? subtotal + tax : subtotal;
}
// ✅ Use default parameters
function fetchProducts(
page: number = 1,
pageSize: number = 20,
sortBy: string = 'name'
): Promise<Product[]> {
return api.get('/products', { page, pageSize, sortBy });
}
// ✅ Return early to reduce nesting
// ❌ Bad
function processPayment(amount: number, user: User): void {
if (amount > 0) {
if (user.balance >= amount) {
if (user.isActive) {
// Process payment
user.balance -= amount;
}
}
}
}
// ✅ Good
function processPayment(amount: number, user: User): void {
if (amount <= 0) {
throw new ValidationError('Amount must be positive');
}
if (user.balance < amount) {
throw new ValidationError('Insufficient balance');
}
if (!user.isActive) {
throw new ValidationError('User account is inactive');
}
user.balance -= amount;
}
interface Order {
items: OrderItem[];
userEmail: string;
total?: number;
}
interface OrderItem {
productId: number;
price: number;
quantity: number;
}
interface User {
id: number;
name: string;
email: string;
role: UserRole;
balance: number;
isActive: boolean;
createdAt: Date;
}
type UserRole = 'admin' | 'user';
interface Product {
id: number;
name: string;
}
declare const database: any;
declare const emailService: any;
declare const inventoryService: any;
declare const api: any;
declare function generateId(): number;
declare function sendWelcomeEmail(email: string): Promise<void>;
declare function sendEmail(email: string, subject: string): void;Practical Production Patterns
Example 1: Repository Pattern
// Generic repository interface
interface IRepository<T, ID = number> {
findById(id: ID): Promise<T | null>;
findAll(filter?: Partial<T>): Promise<T[]>;
create(data: Omit<T, 'id' | 'createdAt' | 'updatedAt'>): Promise<T>;
update(id: ID, data: Partial<T>): Promise<T>;
delete(id: ID): Promise<void>;
}
// User repository implementation
class UserRepository implements IRepository<User> {
constructor(private db: Database) {}
async findById(id: number): Promise<User | null> {
const result = await this.db.query(
'SELECT * FROM users WHERE id = $1',
[id]
);
return result.rows[0] || null;
}
async findAll(filter?: Partial<User>): Promise<User[]> {
let query = 'SELECT * FROM users WHERE 1=1';
const params: any[] = [];
if (filter?.role) {
params.push(filter.role);
query += ` AND role = $${params.length}`;
}
if (filter?.email) {
params.push(filter.email);
query += ` AND email = $${params.length}`;
}
const result = await this.db.query(query, params);
return result.rows;
}
async create(data: Omit<User, 'id' | 'createdAt' | 'updatedAt'>): Promise<User> {
const result = await this.db.query(
`INSERT INTO users (name, email, role)
VALUES ($1, $2, $3)
RETURNING *`,
[data.name, data.email, data.role || 'user']
);
return result.rows[0];
}
async update(id: number, data: Partial<User>): Promise<User> {
const updates: string[] = [];
const params: any[] = [];
if (data.name !== undefined) {
params.push(data.name);
updates.push(`name = $${params.length}`);
}
if (data.email !== undefined) {
params.push(data.email);
updates.push(`email = $${params.length}`);
}
params.push(id);
const query = `
UPDATE users
SET ${updates.join(', ')}, updated_at = NOW()
WHERE id = $${params.length}
RETURNING *
`;
const result = await this.db.query(query, params);
return result.rows[0];
}
async delete(id: number): Promise<void> {
await this.db.query('DELETE FROM users WHERE id = $1', [id]);
}
// Custom methods
async findByEmail(email: string): Promise<User | null> {
const result = await this.db.query(
'SELECT * FROM users WHERE email = $1',
[email]
);
return result.rows[0] || null;
}
}
interface User {
id: number;
name: string;
email: string;
role: string;
createdAt: Date;
updatedAt: Date;
}
interface Database {
query(sql: string, params: any[]): Promise<{ rows: any[] }>;
}Example 2: Service Layer with Dependency Injection
// Service interface
interface IUserService {
getUser(id: number): Promise<User>;
createUser(data: CreateUserDto): Promise<User>;
updateUser(id: number, data: UpdateUserDto): Promise<User>;
deleteUser(id: number): Promise<void>;
}
// Service implementation with DI
class UserService implements IUserService {
constructor(
private userRepository: UserRepository,
private emailService: EmailService,
private logger: Logger
) {}
async getUser(id: number): Promise<User> {
this.logger.info(`Fetching user ${id}`);
const user = await this.userRepository.findById(id);
if (!user) {
throw new NotFoundError('User', id);
}
return user;
}
async createUser(data: CreateUserDto): Promise<User> {
this.logger.info(`Creating user: ${data.email}`);
// Validate
await this.validateNewUser(data);
// Create
const user = await this.userRepository.create(data);
// Send welcome email
await this.emailService.sendWelcomeEmail(user.email, user.name);
this.logger.info(`User created: ${user.id}`);
return user;
}
async updateUser(id: number, data: UpdateUserDto): Promise<User> {
this.logger.info(`Updating user ${id}`);
// Check exists
await this.getUser(id);
// Validate
if (data.email) {
await this.validateEmail(data.email, id);
}
// Update
const user = await this.userRepository.update(id, data);
this.logger.info(`User updated: ${id}`);
return user;
}
async deleteUser(id: number): Promise<void> {
this.logger.info(`Deleting user ${id}`);
// Check exists
await this.getUser(id);
// Delete
await this.userRepository.delete(id);
this.logger.info(`User deleted: ${id}`);
}
private async validateNewUser(data: CreateUserDto): Promise<void> {
// Check email not taken
const existing = await this.userRepository.findByEmail(data.email);
if (existing) {
throw new ValidationError('Email already registered');
}
// Validate email format
if (!this.isValidEmail(data.email)) {
throw new ValidationError('Invalid email format');
}
// Validate name
if (data.name.trim().length < 2) {
throw new ValidationError('Name must be at least 2 characters');
}
}
private async validateEmail(email: string, currentUserId: number): Promise<void> {
const existing = await this.userRepository.findByEmail(email);
if (existing && existing.id !== currentUserId) {
throw new ValidationError('Email already in use');
}
}
private isValidEmail(email: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
}
// Usage with dependency injection
const database = new Database();
const userRepository = new UserRepository(database);
const emailService = new EmailService();
const logger = new Logger();
const userService = new UserService(
userRepository,
emailService,
logger
);
// Now use the service
async function example() {
const user = await userService.createUser({
name: 'Yusuf Ibrahim',
email: 'yusuf@example.com',
role: 'admin'
});
console.log(`Created user: ${user.id}`);
}
interface CreateUserDto {
name: string;
email: string;
role?: string;
}
interface UpdateUserDto {
name?: string;
email?: string;
role?: string;
}
interface User {
id: number;
name: string;
email: string;
role: string;
}
class UserRepository {
constructor(private db: any) {}
async findById(id: number): Promise<User | null> { return null; }
async findByEmail(email: string): Promise<User | null> { return null; }
async create(data: any): Promise<User> { return {} as User; }
async update(id: number, data: any): Promise<User> { return {} as User; }
async delete(id: number): Promise<void> {}
}
class EmailService {
async sendWelcomeEmail(email: string, name: string): Promise<void> {}
}
class Logger {
info(message: string): void {}
}
class Database {}
class NotFoundError extends Error {}
class ValidationError extends Error {}Test Your Knowledge
Type Safety Check
Which code follows TypeScript best practices?
// Option A - Using 'any' everywhere
function processData(data: any): any {
return data.map((item: any) => item.value);
}
const result: any = processData(someData);
// Option B - Proper types with generics
interface DataItem {
id: number;
value: string;
}
function processData<T extends DataItem>(data: T[]): string[] {
return data.map((item) => item.value);
}
const result: string[] = processData(items);
// Option C - Type assertions everywhere
function getData() {
const response = fetch('/api/data') as any;
const data = response.json() as any;
return data as UserData;
}
// Option D - Descriptive names and interfaces
interface User {
id: number;
name: string;
email: string;
}
async function fetchUserById(userId: number): Promise<User> {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error(`Failed to fetch user ${userId}`);
}
const user: User = await response.json();
return user;
}Common TypeScript Error
// Common bad practices
// ❌ Using 'any' to avoid type errors
function processUser(user: any) {
console.log(user.name.toUpperCase());
console.log(user.age + 1);
// What if user doesn't have name or age?
}
// ❌ No error handling
async function loadData() {
const response = await fetch('/api/data');
const data = await response.json();
return data; // What if fetch fails? What if JSON is invalid?
}
// ❌ Ignoring TypeScript errors with @ts-ignore
// @ts-ignore
const value = undefined.toString();
// ❌ Not using strict mode
// TypeScript may allow unsafe operations
// ❌ Magic numbers and unclear logic
function calculate(x: number): number {
if (x > 100) {
return x * 1.15;
}
return x * 1.05;
}❌ Runtime errors, unsafe code, poor maintainability, unclear intent
What's Wrong?
These practices lead to runtime errors, bugs, and unmaintainable code. TypeScript's power comes from using its type system properly.
// ✅ Best practices applied
// ✅ Proper types instead of 'any'
interface User {
id: number;
name: string;
email: string;
age: number;
}
function processUser(user: User): void {
console.log(user.name.toUpperCase());
console.log(user.age + 1);
// TypeScript guarantees these properties exist
}
// ✅ Proper error handling
interface ApiResponse<T> {
data: T;
error?: string;
}
async function loadData<T>(): Promise<T> {
try {
const response = await fetch('/api/data');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result: ApiResponse<T> = await response.json();
if (result.error) {
throw new Error(result.error);
}
return result.data;
} catch (error) {
console.error('Failed to load data:', error);
throw error;
}
}
// ✅ Fix the actual type issue instead of @ts-ignore
function safeToString(value: unknown): string {
if (value === null || value === undefined) {
return '';
}
return String(value);
}
// ✅ Use constants and clear names
const TAX_RATE_PREMIUM = 0.15; // 15% for orders > ₦100,000
const TAX_RATE_STANDARD = 0.05; // 5% for orders ≤ ₦100,000
const PREMIUM_THRESHOLD = 100000;
function calculateTax(orderAmount: number): number {
if (orderAmount > PREMIUM_THRESHOLD) {
return orderAmount * TAX_RATE_PREMIUM;
}
return orderAmount * TAX_RATE_STANDARD;
}
// ✅ Enable strict mode in tsconfig.json
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noUnusedLocals": true,
"noUnusedParameters": true
}
}Solution: Follow best practices for safe, maintainable code
Production Readiness Checklist
- ✅ Strict mode enabled in tsconfig.json
- ✅ No 'any' types - use unknown or specific types
- ✅ Explicit return types for all functions
- ✅ Error handling in all async functions
- ✅ Input validation for all user data
- ✅ Consistent naming conventions throughout
- ✅ Single responsibility - one purpose per function
- ✅ Type guards for runtime validation
- ✅ No unused imports/variables
- ✅ Proper folder structure and organization
- ✅ Documentation for complex logic
- ✅ Tests for critical functionality
Key Takeaways
- Always enable strict mode for maximum type safety
- Follow consistent naming conventions
- Avoid 'any' - use specific types or unknown
- Keep functions small and focused
- Handle errors explicitly and gracefully
- Organize code by feature, not file type
- Use interfaces for object shapes
- Validate data at runtime with type guards
- Write descriptive, self-documenting code
- Follow SOLID principles in architecture
Congratulations!
You've completed the TypeScript tutorial series! You now have a comprehensive understanding of TypeScript, from basic types to advanced patterns, and you're ready to build production-ready applications.
What you've learned:
- TypeScript fundamentals and type system
- Functions, interfaces, and advanced types
- Generics and utility types
- Object-oriented programming with classes
- Modules and code organization
- React with TypeScript
- Async/await and API integration
- Type declarations and best practices
Keep practicing and building! The best way to master TypeScript is to use it in real projects. Start converting your JavaScript projects to TypeScript, contribute to open-source, and keep learning.
Happy coding with TypeScript! 🚀