Falytom logoFALYTOM
Games
ToolsUI ComponentsServicesProductsAbout
menu
  1. Home
  2. /Web Development
  3. /Typescript
  4. /Best Practices
Your Progress0%
0 of 30 completed

TypeScript Topics

Getting Started

  • What is TypeScript?
  • TypeScript vs JavaScript
  • Setting Up TypeScript
  • Your First TypeScript Program

Basic Types

  • Primitive Types
  • Arrays and Tuples
  • Objects and Type Aliases
  • Enums
  • Any, Unknown, and Never

Functions & Interfaces

  • Function Types and Signatures
  • Interfaces
  • Optional and Default Parameters
  • Function Overloading

Advanced Types

  • Union and Intersection Types
  • Type Guards and Narrowing
  • Literal Types
  • Type Assertions and Casting

Generics

  • Introduction to Generics
  • Generic Constraints
  • Utility Types

Classes & OOP

  • Classes and Constructors
  • Access Modifiers
  • Inheritance and Polymorphism
  • Abstract Classes and Interfaces

Practical TypeScript

  • Working with Modules
  • TypeScript with React
  • Async/Await and Promises
  • Working with APIs and JSON
  • Type Declarations and DefinitelyTyped
  • TypeScript Best Practices

TypeScript Best Practices

Code organization, naming conventions, and production-ready TypeScript

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.

tsconfig.json
{
  "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"]
}
strict-mode-examples.ts
// 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

naming-conventions.ts
// ✅ 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

type-safety.ts
// ✅ 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

project-structure.txt
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
types/user.types.ts
// 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;
}
types/index.ts
// 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

error-handling.ts
// ✅ 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

function-best-practices.ts
// ✅ 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

repository-pattern.ts
// 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-layer.ts
// 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

Code with 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.

Corrected Code
// ✅ 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! 🚀

Completed the TypeScript tutorial series! Check this out!

Previous
Type Declarations and DefinitelyTyped

Continue Your Learning Journey

Join 1,000+ developers who have completed this TypeScript series. Get updates on new advanced tutorials, real-world projects, and exclusive resources - completely FREE.

You might also like

UI Components

Ready-made components for your projects

Developer Tools

Boost productivity with handy generators

HTML Game

Learn HTML concepts through interactive gameplay

Join Our Web Development Training Program

Master frontend & backend development with hands-on projects.

TypeScript Tutorials

0 of 30 completed

Your Progress0%

Topics

Getting Started

  • What is TypeScript?
  • TypeScript vs JavaScript
  • Setting Up TypeScript
  • Your First TypeScript Program

Basic Types

  • Primitive Types
  • Arrays and Tuples
  • Objects and Type Aliases
  • Enums
  • Any, Unknown, and Never

Functions & Interfaces

  • Function Types and Signatures
  • Interfaces
  • Optional and Default Parameters
  • Function Overloading

Advanced Types

  • Union and Intersection Types
  • Type Guards and Narrowing
  • Literal Types
  • Type Assertions and Casting

Generics

  • Introduction to Generics
  • Generic Constraints
  • Utility Types

Classes & OOP

  • Classes and Constructors
  • Access Modifiers
  • Inheritance and Polymorphism
  • Abstract Classes and Interfaces

Practical TypeScript

  • Working with Modules
  • TypeScript with React
  • Async/Await and Promises
  • Working with APIs and JSON
  • Type Declarations and DefinitelyTyped
  • TypeScript Best Practices
Falytom logoFALYTOM

Learn technologies like HTML, CSS, JavaScript, React, NodeJS and more through interactive games, tutorials, components and free tools. Transform your businesses with AI chatbots, SEO-optimized websites, and professional development services.

© 2025 Tomilola Group
  • Facebook logo
  • Twitter logo
  • Instagram logo
  • LinkedIn logo
  • GitHub logo
  • YouTube logo