Mastering TypeScript: From Beginner to Advanced

A comprehensive guide to TypeScript covering basic types, advanced features, and best practices for modern development.

typescript javascript programming web-development

Mastering TypeScript: From Beginner to Advanced

TypeScript has become an essential tool in modern web development. It adds static typing to JavaScript, making our code more reliable and maintainable. In this comprehensive guide, we’ll explore TypeScript from the basics to advanced concepts.

What is TypeScript?

TypeScript is a superset of JavaScript that adds optional static typing, classes, and modules. It compiles down to plain JavaScript and provides better tooling support through enhanced IntelliSense and error checking.

Basic Types

Primitive Types

let name: string = "John";
let age: number = 30;
let isActive: boolean = true;
let nothing: null = null;
let undefinedValue: undefined = undefined;
let symbolValue: symbol = Symbol("key");

Array Types

let numbers: number[] = [1, 2, 3, 4, 5];
let strings: Array<string> = ["hello", "world"];
let mixed: (string | number)[] = ["hello", 42, "world"];

Object Types

let person: {
  name: string;
  age: number;
  email?: string; // Optional property
} = {
  name: "John",
  age: 30
};

Interfaces

Interfaces define the structure of objects:

interface User {
  id: number;
  name: string;
  email: string;
  age?: number;
  readonly createdAt: Date;
}

const user: User = {
  id: 1,
  name: "John Doe",
  email: "john@example.com",
  createdAt: new Date()
};

Extending Interfaces

interface Employee extends User {
  department: string;
  salary: number;
}

const employee: Employee = {
  id: 2,
  name: "Jane Smith",
  email: "jane@example.com",
  department: "Engineering",
  salary: 75000,
  createdAt: new Date()
};

Functions

Function Types

function add(a: number, b: number): number {
  return a + b;
}

const multiply: (a: number, b: number) => number = (a, b) => a * b;

Optional and Default Parameters

function greet(name: string, greeting: string = "Hello"): string {
  return `${greeting}, ${name}!`;
}

function log(message: string, ...args: any[]): void {
  console.log(message, ...args);
}

Generics

Generics allow you to create reusable components that work with multiple types:

function identity<T>(arg: T): T {
  return arg;
}

const stringResult = identity<string>("hello");
const numberResult = identity(42); // Type inference

interface Container<T> {
  value: T;
  getValue(): T;
}

class NumberContainer implements Container<number> {
  constructor(public value: number) {}
  
  getValue(): number {
    return this.value;
  }
}

Advanced Types

Union Types

type Status = "loading" | "success" | "error";
type ID = string | number;

function processStatus(status: Status): void {
  switch (status) {
    case "loading":
      console.log("Loading...");
      break;
    case "success":
      console.log("Success!");
      break;
    case "error":
      console.log("Error occurred");
      break;
  }
}

Intersection Types

interface HasName {
  name: string;
}

interface HasAge {
  age: number;
}

type Person = HasName & HasAge;

const person: Person = {
  name: "John",
  age: 30
};

Conditional Types

type NonNullable<T> = T extends null | undefined ? never : T;
type StringOrNumber<T> = T extends string ? string : number;

type Result1 = NonNullable<string | null | undefined>; // string
type Result2 = StringOrNumber<"hello">; // string
type Result3 = StringOrNumber<42>; // number

Utility Types

TypeScript provides several built-in utility types:

interface User {
  id: number;
  name: string;
  email: string;
  age: number;
}

// Partial - makes all properties optional
type PartialUser = Partial<User>;

// Pick - selects specific properties
type UserCredentials = Pick<User, "email" | "name">;

// Omit - excludes specific properties
type UserWithoutId = Omit<User, "id">;

// Record - creates an object type with specific keys and values
type UserRoles = Record<string, "admin" | "user" | "guest">;

Best Practices

1. Use Strict Mode

Enable strict mode in your tsconfig.json:

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

2. Prefer Interfaces for Object Shapes

// Good
interface User {
  name: string;
  age: number;
}

// Avoid
type User = {
  name: string;
  age: number;
};

3. Use 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());
  }
}

4. Leverage Type Inference

// Let TypeScript infer types when possible
const numbers = [1, 2, 3, 4, 5]; // number[]
const user = { name: "John", age: 30 }; // { name: string; age: number }

// Only add explicit types when necessary
function process<T>(items: T[]): T[] {
  return items.reverse();
}

Real-World Example

Here’s a practical example combining multiple concepts:

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

interface User {
  id: number;
  name: string;
  email: string;
}

class UserService {
  private baseUrl: string;

  constructor(baseUrl: string) {
    this.baseUrl = baseUrl;
  }

  async getUser(id: number): Promise<ApiResponse<User>> {
    try {
      const response = await fetch(`${this.baseUrl}/users/${id}`);
      const data = await response.json();
      
      return {
        data,
        status: response.status,
        message: "User retrieved successfully"
      };
    } catch (error) {
      throw new Error(`Failed to fetch user: ${error}`);
    }
  }

  async createUser(userData: Omit<User, "id">): Promise<ApiResponse<User>> {
    try {
      const response = await fetch(`${this.baseUrl}/users`, {
        method: "POST",
        headers: {
          "Content-Type": "application/json"
        },
        body: JSON.stringify(userData)
      });
      
      const data = await response.json();
      
      return {
        data,
        status: response.status,
        message: "User created successfully"
      };
    } catch (error) {
      throw new Error(`Failed to create user: ${error}`);
    }
  }
}

// Usage
const userService = new UserService("https://api.example.com");

userService.getUser(1).then(response => {
  console.log(response.data.name);
});

userService.createUser({
  name: "Jane Doe",
  email: "jane@example.com"
}).then(response => {
  console.log(`Created user with ID: ${response.data.id}`);
});

Conclusion

TypeScript is a powerful tool that can significantly improve your development experience and code quality. By understanding these concepts and following best practices, you’ll be able to write more maintainable and reliable code.

Remember that TypeScript is designed to help you, not hinder you. Start with the basics and gradually incorporate more advanced features as you become comfortable with them.

Happy coding! 🚀