Introduction to TypeScript & Setup
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.
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
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
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.
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.
Objective: Set up a professional TypeScript environment from scratch.
- Initialize a new folder for your project and run
npm init -y. - Install TypeScript globally and initialize the compiler config using
tsc --init. - Open
tsconfig.jsonand ensure"strict": trueis set. - Create a file named
app.ts. - Write a function called
calculateVATthat:- Takes a
price(number) and ataxRate(number). - Returns the final price (number) as a string formatted as: "Total: $[value]".
- Takes a
- Compile the project using
tsc(without filenames, to use the config file) and run the resulting JS file usingnode 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
Variables & Data Types
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"]; |
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
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:
- Create a Type Alias called
SessionStatusthat can only be one of three strings:"active","idle", or"offline". - Create a Tuple called
userSessionthat stores auserId(number) and theSessionStatus. - Create a variable using the
unknowntype to simulate an API response. Write a logic block (usingtypeof) to check if that response is a string; if so, print it in uppercase. - Declare three variables: one using
const(that never changes), one usinglet(that changes once), and prove why you cannot useconstfor the second one.
Functions & Interfaces: Designing Contracts
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).
// 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)}`; };
Professionals distinguish between "I might not provide this" and "Use this if I don't provide it."
- Optional (
?):logMessage(msg: string, userId?: string). TheuserIdcan bestring | 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.
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.
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:
- Create a BaseProduct interface with:
id (readonly),name, andprice. - Create a DigitalProduct interface that
extends BaseProductand addsdownloadUrlandfileSize. - Create a PhysicalProduct interface that
extends BaseProductand addsweightandshippingCost. - Write a function called
calculateFinalPricethat:- Takes a product (of type
BaseProduct). - Takes an optional
discount(number) that defaults to0. - Returns a
stringformatted as: "The final price of [name] is $[total]".
- Takes a product (of type
- Test the function by passing both a Digital and Physical product to it.
Control Flow Mastery
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.
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.
// 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.
// 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.
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
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.
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:
- Create an Interface
Transactionwith:id (number),amount (number),category (string), andstatus (string: "completed" | "pending" | "failed"). - Create an array of at least 5 transactions.
- Step 1 (Filtering): Use
.filter()to create a list of only completed transactions. - Step 2 (Transformation): Use
.map()to create a list of strings that say "Transaction [id] was for $[amount]". - Step 3 (Aggregation): Use
.reduce()to calculate the total sum of all completed transactions. - 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".
Arrays & Tuples Mastery
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.
// 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.
// 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.
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
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
// 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
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:
- Create an Interface
Transactionwith:id (number),amount (number),category (string), andstatus ("completed" | "pending" | "failed"). - Create an array of at least 5 transactions.
- Step 1 (Filtering): Use
.filter()to create a list of only completed transactions. - Step 2 (Transformation): Use
.map()to create a list of strings that say "Transaction [id] was for $[amount]". - Step 3 (Aggregation): Use
.reduce()to calculate the total sum of all completed transactions. - Step 4 (Reporting): Create a tuple
[totalCount: number, totalAmount: number]representing the report summary.
Objects & Classes
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:
brandis a propertystart()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
newkeyword - Not initializing properties properly
- Accessing private properties outside the class
- Misusing the
thiskeyword - Creating overly complex classes
Mini Practice
Try creating the following:
- A
Studentclass with name and grade - A
Carclass with start and stop methods - A class using private properties
- A class with inheritance
- A class using readonly properties
Generics
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:
Trepresents 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
anyinstead 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
Modules & Namespaces
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
Modules & Namespaces
Export & Import
// math.ts export const PI = 3.14; // main.ts import { PI } from "./math";
Type Guards & Assertions
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:
typeofinstanceofinoperator- 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
anytype - 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
typeofchecks - A class example using
instanceof - A DOM element assertion example
- A union type narrowed with the
inoperator
Advanced Types
Utility Types
Partial<T>: All properties optionalReadonly<T>: All properties readonlyPick<T, K>: Select subset of properties
DOM Manipulation
Using TypeScript to safely interact with the browser's DOM API.
const btn = document.getElementById("myBtn") as HTMLButtonElement; btn.addEventListener("click", () => console.log("Clicked!"));
React with TypeScript
Typing Props & State
interface Props { title: string; } const MyComp: React.FC<Props> = ({ title }) => <h1>{title}</h1>;
Capstone Project: Weather App
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
Node.js & Express with TypeScript
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);
Testing & Test-Driven Development
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'); });
TypeScript Design Patterns
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));
Full-Stack Final Project: Task Management App
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;
Full-Stack Final Project
Content coming soon...