Lecture 1 / 12
Lecture 01 Β· Fundamentals

Introduction to TypeScript & Setup

Beginner ~75 min Prerequisite: Basic JavaScript

What is TypeScript?

TypeScript is a strongly typed superset of JavaScript developed by Microsoft. To say it is a "superset" means that any valid JavaScript code is also valid TypeScript code, but TypeScript adds powerful features on top of it.

Professional Concept: Static vs. Dynamic Typing

JavaScript is dynamically typed, meaning a variable can be a string one moment and a number the next. This often leads to "runtime errors" (crashes that happen while the user is using the app).

TypeScript introduces static typing. It checks your types during development (at compile time). If you try to pass a number into a function that expects a string, TypeScript will alert you immediately before the code ever reaches the browser.

The Transpilation Pipeline

A critical point for every professional developer: Browsers cannot run TypeScript files (.ts). They only understand JavaScript (.js).

YourCode.ts $\rightarrow$ TSC (TypeScript Compiler) $\rightarrow$ YourCode.js $\rightarrow$ Browser/Node.js

This process of converting one high-level language to another is called Transpilation. The tool responsible for this is the TypeScript Compiler (tsc).

Installation & Professional Environment Setup

To get started, you need Node.js installed on your machine, as TypeScript is distributed as an NPM package.

1. Global Installation

terminal
npm install -g typescript
tsc --version # Check if installation was successful

2. The Project Configuration (tsconfig.json)

In professional projects, we don't compile files one by one. We use a configuration file to define how the compiler should behave. This is done by initializing a tsconfig.json file.

tsc --init
Pro Tip: Strict Mode

Inside your tsconfig.json, always ensure "strict": true is enabled. This forces you to write higher-quality, safer code and is the standard for enterprise-level TypeScript development.

Anatomy of a TypeScript Program

Let's analyze a professional function implementation. Notice the Type Annotations.

hello.ts
function greet(name: string): string {
    return `Hello, ${name}! Welcome to TypeScript Mastery.`;
}

// Correct usage
console.log(greet("Tinashe")); 

// ❌ Error: Argument of type 'number' is not assignable to parameter of type 'string'
// console.log(greet(123)); 

What is happening here?

  • name: string: This tells TypeScript that the input must be a string.
  • ): string {: This tells TypeScript that the function must return a string.
  • If you try to return a number or pass a number as an argument, the TSC compiler will throw an error before you ever run the code.
🎯 Professional Lab 1.1: Project Initialization

Objective: Set up a professional TypeScript environment from scratch.

  1. Initialize a new folder for your project and run npm init -y.
  2. Install TypeScript globally and initialize the compiler config using tsc --init.
  3. Open tsconfig.json and ensure "strict": true is set.
  4. Create a file named app.ts.
  5. Write a function called calculateVAT that:
    • Takes a price (number) and a taxRate (number).
    • Returns the final price (number) as a string formatted as: "Total: $[value]".
  6. Compile the project using tsc (without filenames, to use the config file) and run the resulting JS file using node app.js.

πŸ’» Try It Yourself - Multi-Language Compiler

Practice TypeScript and many other programming languages right here in your browser! Switch between languages, modify the code, and click "Run" to see results instantly.

πŸ’‘ Practice Tips:

  • Switch to TypeScript in the language selector and try the type-safe examples
  • Experiment with TypeScript's type annotations and interfaces
  • Compare TypeScript syntax with JavaScript to see the differences
  • Try other languages like C#, Java, or Go to compare type systems
  • Use the "Load Example" button to see TypeScript-specific code samples
  • Use Ctrl+Enter to quickly run your code
Lecture 02 Β· Fundamentals

Variables & Data Types

Beginner ~80 min Prerequisite: Lecture 1

Variable Declarations: The Professional Standard

In modern TypeScript/JavaScript, we have three ways to declare variables. However, industry standards dictate a specific hierarchy of use:

  • const: (Default) Use this for every variable. It prevents accidental reassignment and makes the code easier to reason about.
  • let: Use only when you know the value must change (e.g., in a loop or a mathematical counter).
  • var: Avoid entirely. It is function-scoped and can lead to unpredictable bugs due to "hoisting."

Core Primitive Types

TypeScript provides several basic types that allow us to constrain what data a variable can hold.

Type Description Example
string Textual data let username: string = "Dev_User";
number All numbers (integers, floats, hex, binary) let price: number = 99.99;
boolean True or False let isOnline: boolean = true;
string[] An array consisting only of strings let tags: string[] = ["TS", "JS"];
Professional Concept: Type Inference

You don't always have to write the type. If you assign a value immediately, TypeScript is smart enough to infer the type.

let message = "Hello"; // TS automatically knows this is a string

Industry Tip: Use explicit type annotations for function parameters and return types, but rely on inference for local variables to keep the code clean.

Advanced Types: The "Danger Zone"

Sometimes we deal with data where the type is unpredictable (e.g., API responses). This is where any and unknown come in.

1. The any Type (Avoid This)

The any type disables all type checking. It essentially turns TypeScript back into JavaScript.

let data: any = "This could be anything";
data.doSomethingImpossible(); // ❌ No compiler error, but will crash at runtime!

Professional Warning: Overusing any is called "Any-Sickness." It defeats the purpose of using TypeScript. Avoid it at all costs in production code.

2. The unknown Type (The Safe Alternative)

unknown is the type-safe sibling of any. It says "we don't know the type yet," and forces you to check the type before performing any operations on it.

let input: unknown = "Hello";

// console.log(input.toUpperCase()); // ❌ Error: Object is of type 'unknown'

if (typeof input === "string") {
    console.log(input.toUpperCase()); // βœ… Safe! TS now knows 'input' is a string
}

Tuples and Type Aliases

To represent more complex structures, professionals use Tuples and Type Aliases.

Tuples

A Tuple is a fixed-length array where each element has a specific type. This is perfect for coordinates or key-value pairs.

let user: [ number, string ] = [ 1, "Tinashe" ];
// user = ["Tinashe", 1]; // ❌ Error: Wrong order of types

Type Aliases

Instead of repeating a complex type across your app, you can create a "nickname" for it using the type keyword.

type UserID = string | number; // The ID can be either a string OR a number

let currentId: UserID = 101;
currentId = "USR_99"; // Both are valid
🎯 Professional Lab 2.1: The User Identity System

Scenario: You are building a system to track user sessions. You need to ensure that data types are strictly enforced to prevent system crashes.

Requirements:

  1. Create a Type Alias called SessionStatus that can only be one of three strings: "active", "idle", or "offline".
  2. Create a Tuple called userSession that stores a userId (number) and the SessionStatus.
  3. Create a variable using the unknown type to simulate an API response. Write a logic block (using typeof) to check if that response is a string; if so, print it in uppercase.
  4. Declare three variables: one using const (that never changes), one using let (that changes once), and prove why you cannot use const for the second one.
Lecture 03 Β· Fundamentals

Functions & Interfaces: Designing Contracts

Beginner ~100 min Prerequisite: Lecture 2

Type-Safe Functions

In JavaScript, you can pass any argument to any function, which often leads to NaN or undefined errors. In TypeScript, we define a strict contract for what goes in (parameters) and what comes out (return type).

Calculator.ts
// Professional Function Signature: (param: type): returnType
function calculateTotal(price: number, quantity: number): number {
    return price * quantity;
}

// Arrow Function version (Industry Standard for callbacks/components)
const formatCurrency = (amount: number, currency: string = "USD"): string => {
    return `${currency} ${amount.toFixed(2)}`;
};
Professional Insight: Optional vs. Default Parameters

Professionals distinguish between "I might not provide this" and "Use this if I don't provide it."

  • Optional (?): logMessage(msg: string, userId?: string). The userId can be string | undefined.
  • Default: logMessage(msg: string, level: string = "INFO"). If no level is provided, it defaults to "INFO".

Interfaces: The Architectural Blueprint

An Interface is a powerful way to define the "shape" of an object. Think of it as a Contract. Any object that claims to implement that interface must follow its rules.

UserTypes.ts
interface User {
    readonly id: number;     // Cannot be changed after creation
    username: string;
    email: string;
    phoneNumber?: string;    // Optional: Not every user has a phone
    isAdmin: boolean;
}

The Power of readonly

In large applications, accidentally changing an ID or a database key can cause catastrophic bugs. The readonly modifier prevents this at the compiler level.

const user: User = { id: 1, username: "Dev_Pro", email: "pro@dev.com", isAdmin: false };
// user.id = 2; // ❌ Error: Cannot assign to 'id' because it is a read-only property.

Interface Inheritance (Extending)

To avoid repeating code (the DRY principleβ€”Don't Repeat Yourself), interfaces can inherit from other interfaces.

interface Employee {
    employeeId: number;
    department: string;
}

// Manager inherits everything from User AND Employee
interface Manager extends User, Employee {
    teamSize: number;
    officeNumber: number;
}

Interface vs. Type Alias

You will see both interface and type in professional codebases. Here is how to choose:

Feature Interface Type Alias
Object Shapes βœ… Excellent βœ… Good
Extensibility βœ… Via extends βœ… Via Intersections (&)
Union Types ❌ Not possible βœ… Excellent (string | number)
Declaration Merging βœ… Possible (Can add fields later) ❌ Not possible

Professional Rule of Thumb: Use interface for public APIs and object blueprints. Use type for unions, primitives, and complex logic.

🎯 Professional Lab 3.1: The E-Commerce Product Engine

Scenario: You are designing the data layer for a global e-commerce platform. You need to ensure that products and orders are strictly typed to avoid pricing errors.

Requirements:

  1. Create a BaseProduct interface with: id (readonly), name, and price.
  2. Create a DigitalProduct interface that extends BaseProduct and adds downloadUrl and fileSize.
  3. Create a PhysicalProduct interface that extends BaseProduct and adds weight and shippingCost.
  4. Write a function called calculateFinalPrice that:
    • Takes a product (of type BaseProduct).
    • Takes an optional discount (number) that defaults to 0.
    • Returns a string formatted as: "The final price of [name] is $[total]".
  5. Test the function by passing both a Digital and Physical product to it.

Lecture 04 Β· Fundamentals

Control Flow Mastery

Beginner ~110 min Prerequisite: Lecture 3

Professional Control Flow

Control flow in TypeScript is inherited from JavaScript, but we apply professional patterns to keep the code readable and maintainable.

1. The Truthy and Falsy Concept

In TypeScript, every value has an inherent boolean value. Understanding this is critical for writing concise conditions.

Falsy values: false, 0, "" (empty string), null, undefined, and NaN.
Truthy values: Everything else, including [] (empty array) and {} (empty object).

2. The Ternary Operator & Guard Clauses

Professionals avoid "Else-Hell" (deeply nested if-statements). Instead, we use ternaries for simple assignments and guard clauses for validation.

Logic.ts
// Ternary for concise assignment
const status = (age >= 18) ? "Adult" : "Minor";

// Guard Clause: Exit early to avoid nesting
function processPayment(amount: number) {
    if (amount <= 0) return "Invalid Amount"; // Guard
    
    // Proceed with complex logic knowing amount is positive
    return `Processing payment of ${amount}...`;
}

Tuples: Fixed-Structure Arrays

A Tuple is a special type of array with a fixed number of elements, where each element has a known type. This is used in professional development for coordinates, HTTP responses, or key-value pairs.

TupleExample.ts
// Defining a Tuple for an HTTP Response: [StatusCode, Message]
type HttpResponse = [ number, string ];

const successResponse: HttpResponse = [ 200, "OK" ];
const errorResponse: HttpResponse = [ 404, "Not Found" ];

// ❌ Error: Type [string, number] is not assignable to type [number, string]
// const badResponse: HttpResponse = [ "Error", 500 ]; 

Professional Array Manipulation

While for loops work, professional TypeScript developers use Higher-Order Functions. These methods are more declarative, easier to test, and reduce the risk of "off-by-one" errors.

ArrayMethods.ts
interface Product { name: string; price: number; }

const products: Product[] = [
    { name: "Laptop", price: 1000 },
    { name: "Mouse", price: 50 },
    { name: "Keyboard", price: 80 },
];

// 1. .filter() -> Create a new array with items that match a condition
const affordable = products.filter(p => p.price < 100); 
// Result: [{name: "Mouse"...}, {name: "Keyboard"...}]

// 2. .map() -> Transform every item in the array into something else
const productNames = products.map(p => p.name.toUpperCase()); 
// Result: ["LAPTOP", "MOUSE", "KEYBOARD"]

// 3. .reduce() -> Boil the entire array down to a single value (e.g., a sum)
const totalInventoryValue = products.reduce((sum, p) => sum + p.price, 0); 
// Result: 1130
Professional Tip: Immutability

Notice that filter and map do not change the original array; they return a new array. This is called immutability. In modern frameworks like React, mutating your original data directly is a major source of bugs.

🎯 Professional Lab 4.1: The Data Analytics Pipeline

Scenario: You have received a raw list of transactions from a payment gateway. You need to process this data to generate a financial report.

Requirements:

  1. Create an Interface Transaction with: id (number), amount (number), category (string), and status (string: "completed" | "pending" | "failed").
  2. Create an array of at least 5 transactions.
  3. Step 1 (Filtering): Use .filter() to create a list of only completed transactions.
  4. Step 2 (Transformation): Use .map() to create a list of strings that say "Transaction [id] was for $[amount]".
  5. Step 3 (Aggregation): Use .reduce() to calculate the total sum of all completed transactions.
  6. Step 4 (Reporting): Use a Ternary Operator to print a message: If the total sum is > 1000, print "High Volume Day", otherwise print "Standard Volume Day".
Lecture 05 Β· Fundamentals

Arrays & Tuples Mastery

Beginner ~65 min Prerequisite: Lecture 4

Typed Arrays in TypeScript

Unlike JavaScript, TypeScript arrays are strictly typed. Once you define an array's type, it cannot hold elements of a different type. This prevents a whole class of runtime errors.

Arrays.ts
// Basic typed arrays
const numbers: number[] = [1, 2, 3, 4, 5];
const names: string[] = ["Alice", "Bob", "Charlie"];

// ❌ Error: Type 'boolean' is not assignable to type 'string'
// names.push(true);

// Array of objects using interfaces
interface User {
  id: number;
  name: string;
  isActive: boolean;
}

const users: User[] = [
  { id: 1, name: "Alice", isActive: true },
  { id: 2, name: "Bob", isActive: false },
];

// Multi-dimensional arrays
const matrix: number[][] = [
  [1, 2, 3],
  [4, 5, 6],
  [7, 8, 9],
];

Tuples: Fixed-Structure Arrays

A Tuple is a special type of array with a fixed number of elements, where each element has a known type. This is used in professional development for coordinates, HTTP responses, or key-value pairs.

TupleExample.ts
// Defining a Tuple for an HTTP Response: [StatusCode, Message]
type HttpResponse = [ number, string ];

const successResponse: HttpResponse = [ 200, "OK" ];
const errorResponse: HttpResponse = [ 404, "Not Found" ];

// ❌ Error: Type [string, number] is not assignable to type [number, string]
// const badResponse: HttpResponse = [ "Error", 500 ]; 

// Named tuples for clarity
type GeoCoordinate = [lat: number, lng: number];
const nyc: GeoCoordinate = [40.7128, -74.0060];

Professional Array Manipulation

While for loops work, professional TypeScript developers use Higher-Order Functions. These methods are more declarative, easier to test, and reduce the risk of "off-by-one" errors.

ArrayMethods.ts
interface Product { name: string; price: number; category: string; }

const products: Product[] = [
    { name: "Laptop", price: 1000, category: "Electronics" },
    { name: "Mouse", price: 50, category: "Electronics" },
    { name: "Notebook", price: 12, category: "Stationery" },
    { name: "Keyboard", price: 80, category: "Electronics" },
];

// 1. .filter() -> Create a new array with items that match a condition
const electronics = products.filter(p => p.category === "Electronics"); 

// 2. .map() -> Transform every item in the array into something else
const productNames = products.map(p => p.name.toUpperCase()); 

// 3. .reduce() -> Boil the entire array down to a single value (e.g., a sum)
const totalInventoryValue = products.reduce((sum, p) => sum + p.price, 0); 

// 4. .find() -> Get the first item matching a condition
const expensiveItem = products.find(p => p.price > 500);

// 5. .some() / .every() -> Boolean checks
const hasExpensiveItems = products.some(p => p.price > 500); // true
const allHaveNames = products.every(p => p.name.length > 0); // true
Professional Tip: Immutability

Notice that filter and map do not change the original array; they return a new array. This is called immutability. In modern frameworks like React, mutating your original data directly is a major source of bugs.

Destructuring & Spread Operator

Destructuring.ts
// Array destructuring
const [first, second, ...rest] = [10, 20, 30, 40, 50];
console.log(first);  // 10
console.log(rest);   // [30, 40, 50]

// Tuple destructuring
const [statusCode, message]: HttpResponse = [200, "OK"];

// Spread to copy/merge arrays (immutable)
const arr1 = [1, 2, 3];
const arr2 = [4, 5, 6];
const combined = [...arr1, ...arr2]; // [1,2,3,4,5,6]

// Add element immutably
const newArr = [...arr1, 4]; // [1,2,3,4] β€” arr1 is unchanged
🎯 Professional Lab 5.1: The Data Analytics Pipeline

Scenario: You have received a raw list of transactions from a payment gateway. You need to process this data to generate a financial report.

Requirements:

  1. Create an Interface Transaction with: id (number), amount (number), category (string), and status ("completed" | "pending" | "failed").
  2. Create an array of at least 5 transactions.
  3. Step 1 (Filtering): Use .filter() to create a list of only completed transactions.
  4. Step 2 (Transformation): Use .map() to create a list of strings that say "Transaction [id] was for $[amount]".
  5. Step 3 (Aggregation): Use .reduce() to calculate the total sum of all completed transactions.
  6. Step 4 (Reporting): Create a tuple [totalCount: number, totalAmount: number] representing the report summary.
Lecture 06 Β· Fundamentals

Objects & Classes

Beginner ~55 min

Introduction to Object-Oriented Programming

Object-Oriented Programming (OOP) is a programming style based on objects and classes. It helps developers organize code into reusable and structured components.

In TypeScript, classes are used to create blueprints for objects.

  • A class defines properties and behaviors
  • An object is an instance created from a class
  • Classes improve code organization and reusability

Understanding Classes

A class acts like a template for creating objects. It contains variables and methods related to that object.

class Car {
    brand: string = "Toyota";

    start() {
        console.log("Car Started");
    }
}

In this example:

  • brand is a property
  • start() is a method

Creating Objects

Objects are created using the new keyword.

const car1 = new Car();

console.log(car1.brand);

car1.start();

Class Access Modifiers

Access modifiers control how properties and methods can be accessed.

class Animal {

    public name: string;

    private age: number;

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

Public Members

Public members can be accessed from anywhere in the program. By default, class properties are public.

class User {

    public username: string;

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

const user1 = new User("Alex");

console.log(user1.username);

Private Members

Private members can only be accessed inside the class itself. They help protect sensitive data.

class BankAccount {

    private balance: number = 5000;

    showBalance() {
        console.log(this.balance);
    }
}

Attempting to access balance outside the class will cause an error.

Protected Members

Protected members are accessible inside the class and in derived classes.

class Person {

    protected country: string = "USA";
}

class Student extends Person {

    showCountry() {
        console.log(this.country);
    }
}

Constructors

Constructors are special methods used to initialize objects. They run automatically when an object is created.

class Product {

    name: string;

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

const item = new Product("Laptop");

The this Keyword

The this keyword refers to the current object instance.

class Employee {

    name: string;

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

    greet() {
        console.log("Hello " + this.name);
    }
}

Methods Inside Classes

Methods define behaviors for objects.

class Calculator {

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

const calc = new Calculator();

console.log(calc.add(5, 3));

Readonly Properties

Readonly properties cannot be modified after initialization.

class Company {

    readonly companyName: string = "TechCorp";
}

Inheritance

Inheritance allows one class to reuse properties and methods from another class.

class Vehicle {

    move() {
        console.log("Vehicle Moving");
    }
}

class Bike extends Vehicle {

    ringBell() {
        console.log("Bell Ringing");
    }
}

Creating Multiple Objects

A single class can create many different objects.

const dog = new Animal("Tommy");

const cat = new Animal("Kitty");

console.log(dog.name);

console.log(cat.name);

Advantages of Classes

  • Better code organization
  • Reusable components
  • Improved readability
  • Easier maintenance
  • Supports scalable applications

Best Practices

  • Use meaningful class names
  • Keep classes focused on one purpose
  • Use private members for sensitive data
  • Avoid very large classes
  • Reuse logic through inheritance when appropriate

Common Mistakes

  • Forgetting to use the new keyword
  • Not initializing properties properly
  • Accessing private properties outside the class
  • Misusing the this keyword
  • Creating overly complex classes

Mini Practice

Try creating the following:

  • A Student class with name and grade
  • A Car class with start and stop methods
  • A class using private properties
  • A class with inheritance
  • A class using readonly properties
Lecture 07 Β· Core Concepts

Generics

Intermediate ~60 min

Introduction to Generics

Generics allow developers to create reusable and flexible components that work with different data types while maintaining type safety.

Instead of writing separate functions or classes for every data type, generics allow one implementation to handle multiple types.

  • Reduce duplicate code
  • Improve code reusability
  • Provide strong type checking
  • Increase flexibility in applications

Why Generics Matter

Without generics, developers often need multiple versions of the same logic.

function printString(value: string) {
    console.log(value);
}

function printNumber(value: number) {
    console.log(value);
}

Generics solve this problem by allowing one reusable solution.

Reusable Types

Generic functions use type parameters such as T to represent a placeholder type.

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

let output = identity<string>("myString");

In this example:

  • T represents the data type
  • The function accepts and returns the same type
  • The type is decided when the function is called

Type Inference

TypeScript can often automatically detect generic types.

let result = identity("Hello");

TypeScript automatically infers that T is a string.

Generic Functions

Generic functions can work with many different data types.

function showData<T>(data: T): void {
    console.log(data);
}

showData<number>(100);

showData<string>("TypeScript");

showData<boolean>(true);

Generic Arrays

Generics can also be used with arrays.

function getFirst<T>(items: T[]): T {
    return items[0];
}

let firstNumber = getFirst([1, 2, 3]);

let firstText = getFirst(["A", "B", "C"]);

Generic Interfaces

Interfaces can use generics to create reusable object structures.

interface Box<T> {
    value: T;
}

let numberBox: Box<number> = {
    value: 100
};

let textBox: Box<string> = {
    value: "Hello"
};

Generic Classes

Classes can also use generics to work with different data types.

class Storage<T> {

    data: T;

    constructor(value: T) {
        this.data = value;
    }

    getData(): T {
        return this.data;
    }
}

const numberStorage = new Storage<number>(50);

console.log(numberStorage.getData());

Using Multiple Generic Types

A generic component can use more than one type parameter.

function pair<T, U>(first: T, second: U) {
    return [first, second];
}

let resultPair = pair<string, number>("Age", 25);

Generic Constraints

Constraints limit the types that generics can accept.

interface Lengthwise {
    length: number;
}

function logLength<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);

    return arg;
}

This ensures that only values with a length property are allowed.

Generic Utility Example

Generics are commonly used in utility functions and reusable libraries.

function reverseArray<T>(items: T[]): T[] {
    return items.reverse();
}

console.log(reverseArray([1, 2, 3]));

console.log(reverseArray(["A", "B", "C"]));

Benefits of Generics

  • Reusable logic
  • Better type safety
  • Cleaner code structure
  • Reduced duplication
  • Improved maintainability

Common Generic Naming Conventions

Type Meaning
T General type
K Key type
V Value type
U Second type parameter

Best Practices

  • Use generics for reusable logic
  • Keep generic names meaningful when necessary
  • Use constraints to improve safety
  • Avoid unnecessary complexity
  • Prefer type inference when possible

Common Mistakes

  • Using generics when not needed
  • Creating overly complex generic structures
  • Ignoring type constraints
  • Confusing generic types with normal variables
  • Using any instead of proper generics

Mini Practice

Try creating the following:

  • A generic function that returns the last array item
  • A generic class for storing user data
  • A generic interface for API responses
  • A function using two generic parameters
  • A constrained generic function using extends
Lecture 08 Β· Core Concepts

Modules & Namespaces

Intermediate ~45 min

Introduction to Modules

As applications grow larger, organizing code becomes extremely important. Modules help developers split code into separate reusable files.

Each module can contain variables, functions, classes, or interfaces that can be shared across the application.

  • Improves code organization
  • Encourages reusability
  • Makes projects easier to maintain
  • Prevents naming conflicts

What Is a Module?

In TypeScript, any file containing an export statement becomes a module.

Modules allow developers to expose selected parts of a file while keeping other parts private.

Export & Import

The export keyword shares code from one file, while the import keyword brings that code into another file.

// math.ts

export const PI = 3.14;

// main.ts

import { PI } from "./math";

Exporting Functions

Functions can also be exported and reused in other files.

// calculator.ts

export function add(a: number, b: number) {
    return a + b;
}
// app.ts

import { add } from "./calculator";

console.log(add(5, 3));

Exporting Classes

Entire classes can be exported from modules.

// user.ts

export class User {

    name: string;

    constructor(name: string) {
        this.name = name;
    }
}
// main.ts

import { User } from "./user";

const user = new User("Alex");

console.log(user.name);

Default Exports

A module can have one default export. Default exports are imported without curly braces.

// logger.ts

export default function log(message: string) {
    console.log(message);
}
// app.ts

import log from "./logger";

log("Application Started");

Importing Everything

The * symbol imports all exported members from a module.

// math.ts

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

export function subtract(a: number, b: number) {
    return a - b;
}
import * as MathUtils from "./math";

console.log(MathUtils.add(5, 2));

console.log(MathUtils.subtract(10, 4));

Renaming Imports

Imported members can be renamed using the as keyword.

import { add as sum } from "./math";

console.log(sum(2, 3));

Understanding Namespaces

Namespaces provide another way to organize code by grouping related logic together.

Before ES modules became popular, namespaces were commonly used in large TypeScript applications.

Creating a Namespace

namespace Geometry {

    export function area(radius: number) {
        return 3.14 * radius * radius;
    }
}

Using Namespace Members

Namespace members are accessed using dot notation.

console.log(Geometry.area(5));

Namespaces vs Modules

Modules Namespaces
File-based organization Internal organization
Modern approach Older approach
Uses import/export Uses namespace keyword
Preferred in modern projects Less common today

Benefits of Modules

  • Cleaner project structure
  • Reusable components
  • Better scalability
  • Improved maintainability
  • Reduced global variables

Organizing Large Applications

Real-world applications often separate code into modules such as:

  • Authentication modules
  • Database modules
  • Utility modules
  • UI component modules
  • API service modules

Best Practices

  • Keep modules focused on one responsibility
  • Use meaningful file names
  • Avoid exporting unnecessary members
  • Prefer modules over namespaces in modern applications
  • Organize related logic together

Common Mistakes

  • Incorrect file paths in imports
  • Forgetting to export members
  • Using too many global variables
  • Creating overly large modules
  • Mixing namespace and module patterns unnecessarily

Mini Practice

Try creating the following:

  • A math utility module
  • A module exporting multiple functions
  • A default exported logger function
  • A namespace containing geometry functions
  • A multi-file TypeScript project using imports and exports
Lecture 08 Β· Core Concepts

Modules & Namespaces

Intermediate ~45 min

Export & Import

// math.ts
export const PI = 3.14;

// main.ts
import { PI } from "./math";
Lecture 09 Β· Core Concepts

Type Guards & Assertions

Intermediate ~50 min

Introduction to Type Safety

TypeScript provides powerful tools for working safely with different data types. Two important concepts are type guards and type assertions.

These features help developers:

  • Prevent runtime errors
  • Write safer code
  • Handle multiple data types correctly
  • Improve code readability

Understanding Union Types

Type guards are commonly used with union types. A union type allows a variable to store multiple possible types.

let value: string | number;

value = "Hello";

value = 100;

Since the variable can hold different types, TypeScript needs a way to determine the actual type at runtime.

What Are Type Guards?

Type guards are conditions that narrow down a variable's type. They help TypeScript understand what type is currently being used.

Common type guards include:

  • typeof
  • instanceof
  • in operator
  • Custom type guard functions

Type Checking at Runtime

Custom type guard functions return a special type predicate.

function isString(x: any): x is string {

    return typeof x === "string";
}

The expression x is string tells TypeScript that the function checks whether the value is a string.

Using typeof

The typeof operator checks primitive data types.

function printValue(value: string | number) {

    if (typeof value === "string") {

        console.log(value.toUpperCase());

    } else {

        console.log(value.toFixed(2));
    }
}

Inside each block, TypeScript automatically narrows the type.

Using instanceof

The instanceof operator checks object instances created from classes.

class Dog {

    bark() {
        console.log("Woof");
    }
}

class Cat {

    meow() {
        console.log("Meow");
    }
}

function speak(animal: Dog | Cat) {

    if (animal instanceof Dog) {

        animal.bark();

    } else {

        animal.meow();
    }
}

Using the in Operator

The in operator checks whether an object contains a property.

type Admin = {
    permissions: string[];
};

type User = {
    username: string;
};

function check(account: Admin | User) {

    if ("permissions" in account) {

        console.log(account.permissions);
    }
}

Custom Type Guards

Developers can create reusable type guard functions for more complex type checking.

interface Car {
    drive(): void;
}

interface Boat {
    sail(): void;
}

function isCar(vehicle: Car | Boat): vehicle is Car {

    return (vehicle as Car).drive !== undefined;
}

What Are Type Assertions?

Type assertions tell TypeScript to treat a value as a specific type. They do not change the actual runtime type.

Assertions are useful when the developer knows more about a value than TypeScript can infer.

Basic Type Assertions

let value: any = "TypeScript";

let length = (value as string).length;

console.log(length);

Angle Bracket Syntax

Type assertions can also use angle brackets.

let text: any = "Hello";

let result = <string>text;

However, the as syntax is generally preferred, especially in modern frameworks like React.

Assertions with DOM Elements

Type assertions are commonly used when working with HTML elements.

const input = document.getElementById("username") as HTMLInputElement;

input.value = "Admin";

Non-Null Assertion Operator

The ! operator tells TypeScript that a value is not null or undefined.

const button = document.getElementById("submit")!;

button.addEventListener("click", () => {
    console.log("Clicked");
});

Use this carefully because incorrect assumptions may cause runtime errors.

Type Narrowing

Type narrowing means reducing a broad type into a more specific one.

function process(value: string | number) {

    if (typeof value === "number") {

        console.log(value * 2);

    } else {

        console.log(value.toUpperCase());
    }
}

Benefits of Type Guards

  • Safer runtime behavior
  • Better IntelliSense support
  • Improved readability
  • Fewer unexpected errors
  • More predictable code execution

Type Assertions vs Type Casting

Type assertions in TypeScript are different from type casting in other languages.

Assertions only affect TypeScript's understanding of the type. They do not convert values at runtime.

let numberValue = "100" as any;

console.log(numberValue + 1);

This still performs string concatenation because the runtime value remains a string.

Best Practices

  • Prefer type guards over excessive assertions
  • Use assertions only when necessary
  • Write reusable custom type guards
  • Avoid overusing the any type
  • Use non-null assertions carefully

Common Mistakes

  • Using incorrect assertions
  • Ignoring possible null values
  • Overusing any
  • Confusing assertions with actual type conversion
  • Creating unsafe assumptions about runtime values

Mini Practice

Try creating the following:

  • A custom type guard for arrays
  • A function using typeof checks
  • A class example using instanceof
  • A DOM element assertion example
  • A union type narrowed with the in operator
Lecture 10 Β· Advanced

Advanced Types

Advanced ~60 min

Utility Types

  • Partial<T>: All properties optional
  • Readonly<T>: All properties readonly
  • Pick<T, K>: Select subset of properties
Lecture 11 Β· Advanced

DOM Manipulation

Advanced ~55 min

Using TypeScript to safely interact with the browser's DOM API.

const btn = document.getElementById("myBtn") as HTMLButtonElement;
btn.addEventListener("click", () => console.log("Clicked!"));
Lecture 12 Β· Advanced

React with TypeScript

Advanced ~70 min

Typing Props & State

interface Props { title: string; }
const MyComp: React.FC<Props> = ({ title }) => <h1>{title}</h1>;
Lecture 13 Β· Capstone

Capstone Project: Weather App

Advanced ~150 min

Build a weather dashboard using TypeScript, fetching data from a REST API and displaying it with full type safety.

// Project requirements:
// 1. Define interfaces for API responses
// 2. Use async/await with fetch
// 3. Handle loading and error states
// 4. Use Generics for API wrapper functions
Lecture 14 Β· Backend

Node.js & Express with TypeScript

Intermediate ~120 min

Build production-grade REST APIs using TypeScript with Node.js and Express. Learn proper typing for request/response objects, middleware, and error handling.

Setting Up a TS Node Project

// Initialize project
npm init -y
npm install express cors helmet
npm install -D typescript @types/node @types/express ts-node nodemon

// tsconfig.json for Node
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "resolveJsonModule": true
  }
}

Typed Express Server

// src/server.ts
import express, { Request, Response, NextFunction } from 'express';

const app = express();
app.use(express.json());

// Typed request body
interface CreateUserRequest {
  name: string;
  email: string;
  age?: number;
}

app.post('/users', (req: Request<{}, {}, CreateUserRequest>, res: Response) => {
  const { name, email, age } = req.body;
  if (!name || !email) {
    res.status(400).json({ error: 'Name and email are required' });
    return;
  }
  res.status(201).json({ id: 1, name, email, age });
});

app.listen(3000, () => console.log('Server running on port 3000'));

Typed Middleware & Error Handling

// Custom error class
class AppError extends Error {
  constructor(
    public statusCode: number,
    message: string
  ) {
    super(message);
  }
}

// Error middleware
const errorHandler = (
  err: Error,
  req: Request,
  res: Response,
  next: NextFunction
) => {
  if (err instanceof AppError) {
    res.status(err.statusCode).json({ error: err.message });
    return;
  }
  res.status(500).json({ error: 'Internal server error' });
};

app.use(errorHandler);
Lecture 15 Β· Quality

Testing & Test-Driven Development

Intermediate ~100 min

Master testing TypeScript code with Jest and Vitest. Write unit tests, integration tests, and mocks with full type safety.

Testing with Vitest (Recommended)

npm install -D vitest @vitest/ui

// vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
  test: {
    globals: true,
    environment: 'node',
  },
});

// src/utils/calculator.ts
export function add(a: number, b: number): number {
  return a + b;
}

// src/utils/calculator.test.ts
import { describe, it, expect } from 'vitest';
import { add } from './calculator';

describe('Calculator', () => {
  it('adds two numbers correctly', () => {
    expect(add(2, 3)).toBe(5);
  });

  it('handles negative numbers', () => {
    expect(add(-1, -2)).toBe(-3);
  });
});

Mocking & Testing Async Code

// Testing API calls with mocks
import { vi } from 'vitest';

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

const fetchUser = async (id: number): Promise<User> => {
  const res = await fetch(`/api/users/${id}`);
  return res.json();
};

it('fetches user data', async () => {
  const mockUser: User = { id: 1, name: 'Alice' };
  
  global.fetch = vi.fn().mockResolvedValue({
    json: async () => mockUser,
  } as Response);

  const user = await fetchUser(1);
  expect(user.name).toBe('Alice');
});
Lecture 16 Β· Architecture

TypeScript Design Patterns

Advanced ~110 min

Implement classic software design patterns in TypeScript. Build maintainable, scalable applications with proper architectural patterns.

Singleton Pattern

class DatabaseConnection {
  private static instance: DatabaseConnection;
  private connected = false;

  private constructor() {}

  static getInstance(): DatabaseConnection {
    if (!DatabaseConnection.instance) {
      DatabaseConnection.instance = new DatabaseConnection();
    }
    return DatabaseConnection.instance;
  }

  connect(): void {
    if (!this.connected) {
      console.log('Connecting to database...');
      this.connected = true;
    }
  }
}

// Usage
const db1 = DatabaseConnection.getInstance();
const db2 = DatabaseConnection.getInstance();
console.log(db1 === db2); // true β€” same instance

Factory Pattern with Generics

interface Vehicle {
  start(): void;
}

class Car implements Vehicle {
  start() { console.log('Car engine starting...'); }
}

class Motorcycle implements Vehicle {
  start() { console.log('Motorcycle engine starting...'); }
}

// Generic Factory
class VehicleFactory<T extends Vehicle> {
  constructor(private VehicleClass: new () => T) {}

  create(): T {
    return new this.VehicleClass();
  }
}

const carFactory = new VehicleFactory(Car);
const car = carFactory.create();
car.start();

Observer Pattern

type Listener<T> = (data: T) => void;

class EventEmitter<T> {
  private listeners: Map<string, Listener<T>[]> = new Map();

  on(event: string, listener: Listener<T>): void {
    const existing = this.listeners.get(event) || [];
    this.listeners.set(event, [...existing, listener]);
  }

  emit(event: string, data: T): void {
    this.listeners.get(event)?.forEach(l => l(data));
  }
}

// Usage
const emitter = new EventEmitter<{ message: string }>();
emitter.on('message', (data) => console.log(data.message));
Lecture 17 Β· Masterpiece

Full-Stack Final Project: Task Management App

Advanced ~180 min

Build a complete full-stack task management application with React frontend, Express backend, and SQLite database β€” all written in TypeScript.

Project Architecture

task-app/
β”œβ”€β”€ client/                 # React + Vite + TypeScript
β”‚   β”œβ”€β”€ src/
β”‚   β”‚   β”œβ”€β”€ components/
β”‚   β”‚   β”œβ”€β”€ hooks/
β”‚   β”‚   β”œβ”€β”€ types/
β”‚   β”‚   └── api.ts          # Typed API client
β”‚   └── package.json
β”œβ”€β”€ server/                 # Express + TypeScript
β”‚   β”œβ”€β”€ src/
β”‚   β”‚   β”œβ”€β”€ routes/
β”‚   β”‚   β”œβ”€β”€ models/
β”‚   β”‚   β”œβ”€β”€ middleware/
β”‚   β”‚   └── types/
β”‚   └── package.json
└── shared/
    └── types.ts            # Shared types between client & server

Shared Types

// shared/types.ts
export interface Task {
  id: number;
  title: string;
  description: string;
  status: 'pending' | 'in-progress' | 'completed';
  priority: 'low' | 'medium' | 'high';
  createdAt: string;
  dueDate?: string;
}

export interface CreateTaskRequest {
  title: string;
  description: string;
  priority?: 'low' | 'medium' | 'high';
  dueDate?: string;
}

export type ApiResponse<T> = {
  success: true;
  data: T;
} | {
  success: false;
  error: string;
};

Typed API Client

// client/src/api.ts
import { Task, CreateTaskRequest, ApiResponse } from '../../shared/types';

const API_URL = 'http://localhost:3000/api';

async function apiRequest<T>(
  endpoint: string,
  options?: RequestInit
): Promise<ApiResponse<T>> {
  const res = await fetch(`${API_URL}${endpoint}`, {
    headers: { 'Content-Type': 'application/json' },
    ...options,
  });
  return res.json();
}

export const taskApi = {
  getAll: () => apiRequest<Task[]>('/tasks'),
  create: (data: CreateTaskRequest) =>
    apiRequest<Task>('/tasks', {
      method: 'POST',
      body: JSON.stringify(data),
    }),
  update: (id: number, data: Partial<Task>) =>
    apiRequest<Task>(`/tasks/${id}`, {
      method: 'PATCH',
      body: JSON.stringify(data),
    }),
  delete: (id: number) =>
    apiRequest<void>(`/tasks/${id}`, { method: 'DELETE' }),
};

Backend with Validation

// server/src/routes/tasks.ts
import { Router, Request, Response } from 'express';
import { Task, CreateTaskRequest } from '../../shared/types';

const router = Router();
let tasks: Task[] = [];
let nextId = 1;

router.post('/', (req: Request<{}, {}, CreateTaskRequest>, res) => {
  const { title, description, priority = 'medium' } = req.body;

  if (!title?.trim()) {
    res.status(400).json({ success: false, error: 'Title is required' });
    return;
  }

  const task: Task = {
    id: nextId++,
    title,
    description: description || '',
    status: 'pending',
    priority,
    createdAt: new Date().toISOString(),
  };

  tasks.push(task);
  res.status(201).json({ success: true, data: task });
});

export default router;
Lecture 18 Β· Architecture

Full-Stack Final Project

Advanced ~90 min Requires: All Previous

Content coming soon...