The Complete TypeScript Handbook

April 16, 2025 (2mo ago)

The Complete TypeScript Handbook

I recently dove deep into TypeScript, and honestly, it's been a game-changer for my development workflow. After spending months exploring everything from basic types to advanced patterns, I wanted to share what I've learned and the key insights that made the biggest difference. Here’s an improved version with better flow, clarity, and tone:

Reader Note - ⏰ Quick Heads-Up

This is a complete, hands-on guide—set aside 30–45 minutes of focused reading time. It’s packed with practical insights and real-world examples to help you truly master TypeScript.

Don’t be put off by the length—a big chunk is made up of simple, easy-to-follow code snippets.

Grab a coffee ☕️ and let’s dive in.

Why TypeScript Matters: The Great Type Divide

Before we dive into TypeScript specifics, let's understand the fundamental difference between programming languages and why TypeScript exists.

Strongly Typed vs Loosely Typed Languages

Programming languages fall into two main categories when it comes to type handling:

Aspect Strongly Typed Loosely Typed
Examples Java, C++, C#, Rust, TypeScript JavaScript, Python, PHP, Perl
Type Checking Enforced at compile-time Determined at runtime
Error Detection Catch errors before code runs Errors surface during execution
Development Speed Slower initially, faster long-term Faster to write initially
Tooling Support Excellent IDE support & autocomplete Limited compared to strongly typed
Refactoring Safer and easier More error-prone
Flexibility Less flexible, more predictable Highly flexible but error-prone
Runtime Errors Fewer type-related runtime errors Can lead to unexpected runtime errors

Code Example Comparison

Strongly Typed (C++):

#include <iostream>
 
int main() {
  int number = 10;
  number = "text";  // ❌ Compilation Error!
  return 0;
}

Loosely Typed (JavaScript):

function main() {
  let number = 10;
  number = "text"; // ✅ Works fine (but may cause issues later)
  return number;
}

How TypeScript Works

Here's the crucial thing to understand: TypeScript never runs in your browser. The workflow looks like this:

TypeScript Code (.ts) → TypeScript Compiler (tsc) → JavaScript Code (.js) → Browser/Node.js

The TypeScript compiler does two important things:

  1. Type checking - Catches errors before runtime
  2. Transpilation - Converts TS code to JS code

Setting Up Your First TypeScript Project

Let's get our hands dirty with a practical setup. I'll walk you through creating a TypeScript Node.js application from scratch.

Step 1: Install TypeScript Globally

npm install -g typescript

Step 2: Initialize Your Project

mkdir my-typescript-app
cd my-typescript-app
npm init -y
npx tsc --init

This creates two important files:

Step 3: Write Your First TypeScript Code

Create a file called app.ts:

const message: string = "Hello, TypeScript!";
const count: number = 42;
const isActive: boolean = true;
 
console.log(`${message} Count: ${count}, Active: ${isActive}`);

Step 4: Compile and Run

# Compile TypeScript to JavaScript
tsc -b
 
# Run the generated JavaScript
node app.js

Step 5: See TypeScript in Action

Now let's see TypeScript catch an error. Modify your app.ts:

let count: number = 42;
count = "Vishal Rajput"; // ❌ TypeScript will catch this error!
console.log(count);

Try compiling again:

tsc -b

You'll see an error message, and no JavaScript file will be generated. This is TypeScript protecting you from runtime errors!

Mastering Basic Types

TypeScript provides several fundamental types that form the building blocks of your applications.

Primitive Types

// Numbers - integers and floats
let age: number = 25;
let price: number = 99.99;
 
// Strings - text data
let firstName: string = "Vishal";
let lastName: string = "Rajput";
 
// Booleans - true/false values
let isStudent: boolean = true;
let hasJob: boolean = false;
 
// Null and Undefined
let emptyValue: null = null;
let notAssigned: undefined = undefined;

Function Types and Parameters

One of TypeScript's most powerful features is adding types to functions. Let me show you some practical examples:

Basic Function with Typed Parameters

function greetUser(firstName: string): void {
  console.log(`Hello, ${firstName}! Welcome to our platform.`);
}
 
// Usage
greetUser("Vishal"); // ✅ Works
greetUser(123); // ❌ Error: Argument of type 'number' is not assignable to parameter of type 'string'

Functions with Return Types

function calculateSum(a: number, b: number): number {
  return a + b;
}
 
function checkAge(age: number): boolean {
  return age >= 18;
}
 
// Usage examples
const total = calculateSum(10, 15); // total is inferred as number
const isAdult = checkAge(21); // isAdult is inferred as boolean
 
console.log(`Total: ${total}, Is Adult: ${isAdult}`);

Higher-Order Functions

TypeScript really shines when working with functions that take other functions as parameters:

function executeAfterDelay(callback: () => void): void {
  setTimeout(callback, 1000);
}
 
// Usage
executeAfterDelay(() => {
  console.log("This runs after 1 second!");
});

Configuring TypeScript: The tsconfig.json File

The tsconfig.json file is your control center for TypeScript compilation. Let me walk you through the most important options:

Essential Configuration Options

{
  "compilerOptions": {
    "target": "ES2020", // JavaScript version to compile to
    "rootDir": "src", // Where your TypeScript files live
    "outDir": "dist", // Where compiled JavaScript goes
    "noImplicitAny": true, // Catch implicit 'any' types
    "removeComments": true, // Clean up output files
    "strict": true // Enable all strict type checking
  }
}

Target Option Deep Dive

The target option determines which JavaScript version your TypeScript compiles to. Here's a practical example:

TypeScript code:

const greetUser = (name: string) => `Hello, ${name}!`;

Compiled to ES5:

"use strict";
var greetUser = function (name) {
  return "Hello, ".concat(name, "!");
};

Compiled to ES2020:

"use strict";
const greetUser = (name) => `Hello, ${name}!`;

Project Structure Best Practices

I recommend organizing your project like this:

my-typescript-app/
├── src/           # TypeScript source files
├── dist/          # Compiled JavaScript files
├── tsconfig.json  # TypeScript configuration
└── package.json   # Node.js configuration

Interfaces: Defining Object Shapes

Interfaces are one of TypeScript's most powerful features. They let you define contracts for object structures, making your code more predictable and maintainable.

Basic Interface Usage

Let's say you're building a user management system:

interface User {
  firstName: string;
  lastName: string;
  email: string;
  age: number;
  isActive: boolean;
}
 
// Now you can use this interface to type objects
const user: User = {
  firstName: "Vishal",
  lastName: "Rajput",
  email: "vishal.rajput@email.com",
  age: 28,
  isActive: true,
};

Practical Interface Examples

User Age Verification System

interface User {
  firstName: string;
  lastName: string;
  email: string;
  age: number;
}
 
function isEligibleToVote(user: User): boolean {
  return user.age >= 18;
}
 
function getUserFullName(user: User): string {
  return `${user.firstName} ${user.lastName}`;
}
 
// Usage
const vishal: User = {
  firstName: "Vishal",
  lastName: "Rajput",
  email: "vishal@example.com",
  age: 25,
};
 
console.log(`${getUserFullName(vishal)} can vote: ${isEligibleToVote(vishal)}`);

Todo Application Interface

interface Todo {
  id: number;
  title: string;
  description: string;
  completed: boolean;
  createdAt: Date;
}
 
interface TodoProps {
  todo: Todo;
  onToggle: (id: number) => void;
  onDelete: (id: number) => void;
}
 
// React component using the interface
function TodoItem({ todo, onToggle, onDelete }: TodoProps) {
  return (
    <div className={`todo-item ${todo.completed ? "completed" : ""}`}>
      <h3>{todo.title}</h3>
      <p>{todo.description}</p>
      <button onClick={() => onToggle(todo.id)}>
        {todo.completed ? "Mark Incomplete" : "Mark Complete"}
      </button>
      <button onClick={() => onDelete(todo.id)}>Delete</button>
    </div>
  );
}

Implementing Interfaces with Classes

Interfaces can also define contracts that classes must follow:

interface Employee {
  name: string;
  employeeId: number;
  department: string;
  calculateSalary(): number;
  getDetails(): string;
}
 
class Developer implements Employee {
  name: string;
  employeeId: number;
  department: string;
  private hourlyRate: number;
  private hoursWorked: number;
 
  constructor(name: string, id: number, hourlyRate: number) {
    this.name = name;
    this.employeeId = id;
    this.department = "Engineering";
    this.hourlyRate = hourlyRate;
    this.hoursWorked = 40; // Default full-time hours
  }
 
  calculateSalary(): number {
    return this.hourlyRate * this.hoursWorked;
  }
 
  getDetails(): string {
    return `${this.name} (ID: ${this.employeeId}) - ${this.department}`;
  }
}
 
class Manager implements Employee {
  name: string;
  employeeId: number;
  department: string;
  private baseSalary: number;
  private bonus: number;
 
  constructor(
    name: string,
    id: number,
    department: string,
    baseSalary: number
  ) {
    this.name = name;
    this.employeeId = id;
    this.department = department;
    this.baseSalary = baseSalary;
    this.bonus = 0;
  }
 
  calculateSalary(): number {
    return this.baseSalary + this.bonus;
  }
 
  getDetails(): string {
    return `${this.name} (ID: ${this.employeeId}) - Manager, ${this.department}`;
  }
}

Types: Beyond Basic Interfaces

While interfaces are great for object shapes, TypeScript's type keyword offers more flexibility and power.

Basic Type Definitions

type User = {
  firstName: string;
  lastName: string;
  age: number;
  email: string;
};
 
type UserId = string | number; // Union type

Union Types: Handling Multiple Possibilities

Union types are incredibly useful when dealing with values that could be one of several types:

type ID = string | number;
 
function printUserID(id: ID): void {
  console.log(`User ID: ${id}`);
}
 
// Both of these work
printUserID(12345); // number
printUserID("USER_12345"); // string

Real-World Union Type Example

type APIResponse =
  | {
      success: true;
      data: any;
    }
  | {
      success: false;
      error: string;
    };
 
function handleAPIResponse(response: APIResponse): void {
  if (response.success) {
    console.log("Data received:", response.data);
  } else {
    console.error("API Error:", response.error);
  }
}

Intersection Types: Combining Multiple Types

Intersection types let you combine multiple types into one:

type Employee = {
  name: string;
  employeeId: number;
  startDate: Date;
};
 
type Manager = {
  department: string;
  teamSize: number;
  budget: number;
};
 
type TeamLead = Employee & Manager;
 
const vishalTeamLead: TeamLead = {
  name: "Vishal Rajput",
  employeeId: 1001,
  startDate: new Date("2023-01-15"),
  department: "Engineering",
  teamSize: 5,
  budget: 100000,
};

When to Use Types vs Interfaces

Use Interfaces when:

Use Types when:

Working with Arrays in TypeScript

Arrays in TypeScript are straightforward but powerful. Here's how to work with them effectively:

Basic Array Syntax

// Array of numbers
const numbers: number[] = [1, 2, 3, 4, 5];
 
// Array of strings
const names: string[] = ["Vishal", "Rajput", "Developer"];
 
// Alternative syntax (both are equivalent)
const ages: Array<number> = [25, 30, 35];

Practical Array Examples

Finding Maximum Value

function findMaximum(numbers: number[]): number {
  if (numbers.length === 0) {
    throw new Error("Array cannot be empty");
  }
 
  return Math.max(...numbers);
}
 
// Usage
const scores = [85, 92, 78, 96, 88];
console.log(`Highest score: ${findMaximum(scores)}`);

Filtering Users by Age

interface User {
  firstName: string;
  lastName: string;
  age: number;
  email: string;
}
 
function filterAdultUsers(users: User[]): User[] {
  return users.filter((user) => user.age >= 18);
}
 
// Usage
const allUsers: User[] = [
  {
    firstName: "Vishal",
    lastName: "Rajput",
    age: 25,
    email: "vishal@example.com",
  },
  { firstName: "John", lastName: "Doe", age: 17, email: "john@example.com" },
  { firstName: "Jane", lastName: "Smith", age: 22, email: "jane@example.com" },
];
 
const adultUsers = filterAdultUsers(allUsers);
console.log(`Found ${adultUsers.length} adult users`);

Enums: Creating Named Constants

Enums help you create a set of named constants, making your code more readable and maintainable.

Basic Enum Usage

enum Direction {
  Up,
  Down,
  Left,
  Right,
}
 
function movePlayer(direction: Direction): void {
  switch (direction) {
    case Direction.Up:
      console.log("Moving up!");
      break;
    case Direction.Down:
      console.log("Moving down!");
      break;
    case Direction.Left:
      console.log("Moving left!");
      break;
    case Direction.Right:
      console.log("Moving right!");
      break;
  }
}
 
// Usage
movePlayer(Direction.Up);

Custom Enum Values

enum HTTPStatus {
  OK = 200,
  BadRequest = 400,
  Unauthorized = 401,
  NotFound = 404,
  InternalServerError = 500,
}
 
enum LogLevel {
  DEBUG = "DEBUG",
  INFO = "INFO",
  WARN = "WARN",
  ERROR = "ERROR",
}
 
function logMessage(level: LogLevel, message: string): void {
  console.log(`[${level}] ${new Date().toISOString()}: ${message}`);
}
 
// Usage
logMessage(LogLevel.INFO, "Application started successfully");
logMessage(LogLevel.ERROR, "Database connection failed");

Real-World Enum Example: API Response Handler

enum APIEndpoints {
  USERS = "/api/users",
  POSTS = "/api/posts",
  COMMENTS = "/api/comments",
  AUTH = "/api/auth",
}
 
enum RequestMethod {
  GET = "GET",
  POST = "POST",
  PUT = "PUT",
  DELETE = "DELETE",
}
 
interface APIRequest {
  endpoint: APIEndpoints;
  method: RequestMethod;
  data?: any;
}
 
function makeAPICall(request: APIRequest): void {
  console.log(`Making ${request.method} request to ${request.endpoint}`);
 
  if (request.data) {
    console.log("Request data:", request.data);
  }
}
 
// Usage
makeAPICall({
  endpoint: APIEndpoints.USERS,
  method: RequestMethod.POST,
  data: { name: "Vishal", email: "vishal@example.com" },
});

Generics: Writing Reusable Code

Generics are one of TypeScript's most powerful features. They allow you to write flexible, reusable code while maintaining type safety.

The Problem Generics Solve

Imagine you need a function that returns the first element of an array. Without generics, you might write:

// Bad approach - separate functions for each type
function getFirstString(arr: string[]): string {
  return arr[0];
}
 
function getFirstNumber(arr: number[]): number {
  return arr[0];
}
 
// Or worse - using 'any' (loses type safety)
function getFirstElement(arr: any[]): any {
  return arr[0];
}

Generic Solution

function getFirstElement<T>(arr: T[]): T {
  return arr[0];
}
 
// TypeScript infers the correct type automatically
const firstString = getFirstElement(["Vishal", "Rajput", "Developer"]); // string
const firstNumber = getFirstElement([1, 2, 3, 4, 5]); // number
const firstBoolean = getFirstElement([true, false]); // boolean
 
// You can also be explicit about the type
const explicitString = getFirstElement<string>(["Hello", "World"]);

Practical Generic Examples

Generic API Response Handler

interface APIResponse<T> {
  data: T;
  success: boolean;
  message: string;
}
 
interface User {
  id: number;
  name: string;
  email: string;
}
 
interface Post {
  id: number;
  title: string;
  content: string;
  authorId: number;
}
 
function handleAPIResponse<T>(response: APIResponse<T>): T | null {
  if (response.success) {
    console.log(response.message);
    return response.data;
  } else {
    console.error("API Error:", response.message);
    return null;
  }
}
 
// Usage with different data types
const userResponse: APIResponse<User> = {
  data: { id: 1, name: "Vishal Rajput", email: "vishal@example.com" },
  success: true,
  message: "User fetched successfully",
};
 
const postResponse: APIResponse<Post[]> = {
  data: [
    {
      id: 1,
      title: "TypeScript Guide",
      content: "Learning TS...",
      authorId: 1,
    },
  ],
  success: true,
  message: "Posts fetched successfully",
};
 
const user = handleAPIResponse(userResponse); // User | null
const posts = handleAPIResponse(postResponse); // Post[] | null

Generic Storage Class

class Storage<T> {
  private items: T[] = [];
 
  add(item: T): void {
    this.items.push(item);
  }
 
  get(index: number): T | undefined {
    return this.items[index];
  }
 
  getAll(): T[] {
    return [...this.items];
  }
 
  remove(index: number): T | undefined {
    return this.items.splice(index, 1)[0];
  }
 
  size(): number {
    return this.items.length;
  }
}
 
// Usage with different types
const stringStorage = new Storage<string>();
stringStorage.add("Vishal");
stringStorage.add("Rajput");
 
const numberStorage = new Storage<number>();
numberStorage.add(42);
numberStorage.add(100);
 
const userStorage = new Storage<User>();
userStorage.add({ id: 1, name: "Vishal", email: "vishal@example.com" });

Modules: Organizing Your Code

As your TypeScript applications grow, organizing code into modules becomes essential. TypeScript follows the ES6 module system.

Named Exports and Imports

math.ts

export function add(x: number, y: number): number {
  return x + y;
}
 
export function subtract(x: number, y: number): number {
  return x - y;
}
 
export function multiply(x: number, y: number): number {
  return x * y;
}
 
export const PI = 3.14159;

calculator.ts

import { add, subtract, multiply, PI } from "./math";
 
export class Calculator {
  add(x: number, y: number): number {
    return add(x, y);
  }
 
  subtract(x: number, y: number): number {
    return subtract(x, y);
  }
 
  multiply(x: number, y: number): number {
    return multiply(x, y);
  }
 
  calculateCircleArea(radius: number): number {
    return PI * radius * radius;
  }
}

Default Exports

user.ts

interface User {
  id: number;
  name: string;
  email: string;
}
 
export default class UserManager {
  private users: User[] = [];
 
  addUser(user: User): void {
    this.users.push(user);
  }
 
  getUserById(id: number): User | undefined {
    return this.users.find((user) => user.id === id);
  }
 
  getAllUsers(): User[] {
    return [...this.users];
  }
}

app.ts

import UserManager from "./user";
import { Calculator } from "./calculator";
 
const userManager = new UserManager();
const calculator = new Calculator();
 
userManager.addUser({
  id: 1,
  name: "Vishal Rajput",
  email: "vishal@example.com",
});
 
console.log("Users:", userManager.getAllUsers());
console.log("10 + 5 =", calculator.add(10, 5));

Advanced TypeScript Patterns

Utility Types

TypeScript provides several built-in utility types that make working with existing types easier:

interface User {
  id: number;
  name: string;
  email: string;
  age: number;
  isActive: boolean;
}
 
// Partial - makes all properties optional
type PartialUser = Partial<User>;
function updateUser(id: number, updates: PartialUser): void {
  // Implementation here
}
 
// Pick - select specific properties
type UserSummary = Pick<User, "id" | "name" | "email">;
 
// Omit - exclude specific properties
type CreateUserData = Omit<User, "id">;
 
// Required - makes all properties required
type RequiredUser = Required<PartialUser>;

Conditional Types

type NonNullable<T> = T extends null | undefined ? never : T;
 
type UserEmail = NonNullable<string | null>; // string

Best Practices and Pro Tips

1. Use Strict Mode

Always enable strict mode in your tsconfig.json:

{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true
  }
}

2. Prefer Types for Complex Unions

// Good
type Status = "loading" | "success" | "error";
 
// Less ideal for this use case
interface StatusInterface {
  status: "loading" | "success" | "error";
}

3. Use Readonly for Immutable Data

interface ReadonlyUser {
  readonly id: number;
  readonly email: string;
  name: string; // This can still be modified
}

4. Leverage Type Guards

function isString(value: unknown): value is string {
  return typeof value === "string";
}
 
function processValue(value: unknown): void {
  if (isString(value)) {
    // TypeScript knows value is string here
    console.log(value.toUpperCase());
  }
}

Common Pitfalls to Avoid

1. Overusing any

// Bad
function processData(data: any): any {
  return data.something.else;
}
 
// Good
interface DataStructure {
  something: {
    else: string;
  };
}
 
function processData(data: DataStructure): string {
  return data.something.else;
}

2. Not Using Union Types When Appropriate

// Bad
function getId(): any {
  return Math.random() > 0.5 ? 123 : "user_123";
}
 
// Good
function getId(): number | string {
  return Math.random() > 0.5 ? 123 : "user_123";
}

3. Ignoring Null/Undefined Checks

// Bad
function getUserName(user: User): string {
  return user.name.toUpperCase(); // What if name is undefined?
}
 
// Good
function getUserName(user: User): string {
  return user.name?.toUpperCase() ?? "Unknown User";
}

Quick Reference Cheat Sheet

Basic Types

let num: number = 42;
let str: string = "Hello";
let bool: boolean = true;
let arr: number[] = [1, 2, 3];
let tuple: [string, number] = ["age", 25];

Function Types

function greet(name: string): string {
  return `Hello ${name}`;
}
const add = (a: number, b: number): number => a + b;
type Handler = (event: string) => void;

Object Types

interface User {
  name: string;
  age: number;
}
type Point = { x: number; y: number };

Advanced Types

type ID = string | number; // Union
type Employee = Person & Worker; // Intersection
type Status = "idle" | "loading"; // Literal types

Conclusion

TypeScript has fundamentally changed how I approach JavaScript development. The static typing, excellent tooling, and enhanced developer experience make it an invaluable tool for building robust applications.

Remember, the goal isn't to use every TypeScript feature in every project. Start with the basics - proper typing for functions and objects - and gradually incorporate more advanced features as you become comfortable.

Frequently Asked Questions

1. When should I use TypeScript over JavaScript?

Use TypeScript when:

Stick with JavaScript for:

2. What's the difference between interface and type?

Use interface when:

Use type when:

3. How do I handle null and undefined in TypeScript?

Enable strictNullChecks in your tsconfig.json and use:

4. What are generics and when should I use them?

Generics allow you to create reusable components that work with multiple types while maintaining type safety. Use them when:

5. How do I migrate an existing JavaScript project to TypeScript?

  1. Install TypeScript: npm install -D typescript @types/node
  2. Create tsconfig.json: npx tsc --init
  3. Rename .js files to .ts gradually
  4. Start with "noImplicitAny": false and enable it later
  5. Add types incrementally, starting with function parameters and return types
  6. Install type definitions for external libraries: npm install -D @types/library-name

6. What are the most common TypeScript compilation errors?

7. How do I type React components in TypeScript?

import React from "react";
 
interface Props {
  name: string;
  age?: number;
  onSave: (data: string) => void;
}
 
const UserCard: React.FC<Props> = ({ name, age, onSave }) => {
  return (
    <div>
      <h2>{name}</h2>
      {age && <p>Age: {age}</p>}
      <button onClick={() => onSave(name)}>Save</button>
    </div>
  );
};

8. Should I use classes or functions with TypeScript?

Both have their place:

9. How do I properly type async functions and Promises?

TypeScript automatically infers Promise types, but you can be explicit:

// Function returns Promise<string>
async function fetchUserName(id: number): Promise<string> {
  const response = await fetch(`/api/users/${id}`);
  const user = await response.json();
  return user.name;
}
 
// Using with try-catch
async function safeApiCall(): Promise<string | null> {
  try {
    const result = await fetchUserName(123);
    return result;
  } catch (error) {
    console.error("API call failed:", error);
    return null;
  }
}
 
// Typing Promise.all
const results = await Promise.all([
  fetchUserName(1), // Promise<string>
  fetchUserName(2), // Promise<string>
  fetchUserName(3), // Promise<string>
]); // results is string[]

10. What's the best way to handle complex nested object types?

Use a combination of interfaces, utility types, and type composition:

// Base interfaces
interface Address {
  street: string;
  city: string;
  zipCode: string;
  country: string;
}
 
interface User {
  id: number;
  name: string;
  email: string;
  address: Address;
  preferences: {
    notifications: boolean;
    theme: "light" | "dark";
    language: string;
  };
}
 
// Utility types for partial updates
type UserUpdate = Partial<Pick<User, "name" | "email">>;
type AddressUpdate = Partial<Address>;
 
// Nested partial for complex updates
type UserProfileUpdate = {
  user?: UserUpdate;
  address?: AddressUpdate;
  preferences?: Partial<User["preferences"]>;
};
 
// Using mapped types for form data
type UserFormData = {
  [K in keyof User]: User[K] extends object
    ? string // Convert nested objects to strings for form handling
    : User[K];
};

"Take a break if you made it through the blog in one go — well done! Keep practicing TypeScript consistently to sharpen your skills."