Let's be very clear about one thing first.
Most people jump straight into Angular and then struggle. They don't know why things work the way they do. They copy-paste code without understanding it. Then they get stuck on something simple and have no idea how to fix it.
We are NOT doing that.
We are going to build a rock-solid foundation first. By the time you write your first Angular component, you will understand exactly what is happening and why.
Why TypeScript First?
Angular is written 100% in TypeScript. Every file you create will be a .ts file. If you don't understand TypeScript, you will be confused by Angular syntax from line one.
But more importantly — TypeScript teaches you to think like a structured programmer. It forces you to think about what kind of data you're working with before you work with it. This habit alone will make you a significantly better developer.
Setting Up Your Environment
Before writing a single line of code, let's set up your workspace properly.
Step 1 — Install Node.js
Go to nodejs.org and download the LTS version. LTS means Long Term Support — it's the stable version. Don't download the latest/current version, it may have bugs.
After installing, open your terminal and verify:
node --version
npm --version
You should see version numbers. If you do, Node is installed correctly.
Node.js itself is a JavaScript runtime — it lets you run JavaScript outside the browser. But more importantly for us right now, it comes with npm (Node Package Manager), which is how we install Angular and TypeScript.
Step 2 — Install TypeScript
npm install -g typescript
The -g flag means global — install it on your entire computer, not just one project.
Verify:
tsc --version
tsc stands for TypeScript Compiler. It converts your TypeScript code into regular JavaScript that browsers understand.
Step 3 — Install a Code Editor
Use Visual Studio Code. It's free, it's built by Microsoft (the same people who made TypeScript), and it has the best TypeScript support of any editor.
Download from code.visualstudio.com
Essential VS Code Extensions to install:
- Angular Language Service — gives you intellisense inside Angular HTML templates
- Prettier — Code Formatter — auto-formats your code
- ESLint — catches code quality issues
- Auto Rename Tag — renames closing tags automatically
Step 4 — Create your TypeScript practice folder
mkdir typescript-practice
cd typescript-practice
Create a file called index.ts inside it. This is where we'll practice everything.
To run any TypeScript file:
tsc index.ts ← this compiles index.ts into index.js
node index.js ← this runs the compiled file
Or install ts-node to run TypeScript directly without manually compiling:
npm install -g ts-node
ts-node index.ts ← compile + run in one command
We'll use ts-node throughout this guide because it's faster.
TypeScript — Complete Deep Dive
Chapter 1 — The Type System — Why It Exists and How It Thinks
Before we write any code, let's understand the problem TypeScript is solving.
In JavaScript, you can do this:
let userId = 5;
userId = "hello"; // No error. JavaScript is fine with this.
userId = true; // Still no error.
userId = { name: "x" }; // JavaScript doesn't care.
This seems flexible but it's actually dangerous. Imagine you have a function that calculates a discount based on userId. If userId is accidentally a string, your calculation breaks silently. No error. Just wrong results. And you won't know why until a user reports a bug.
TypeScript fixes this by adding a type system. You declare what kind of value a variable holds, and TypeScript makes sure you never accidentally put the wrong kind of value in it.
let userId: number = 5;
userId = "hello"; // ERROR: Type 'string' is not assignable to type 'number'
Now the error shows up immediately in your editor — before you even run the code.
This is called static type checking. Static means it happens at compile time (when you write code), not at runtime (when the code runs). Catching bugs at compile time is infinitely better than catching them in production.
Chapter 2 — Primitive Types — The Building Blocks
TypeScript has 7 primitive types. Let's cover all of them deeply.
2.1 — string
Represents any text value.
let firstName: string = "Rahul";
let lastName: string = 'Sharma'; // single quotes also work
let greeting: string = `Hello, ${firstName} ${lastName}!`; // template literals work too
console.log(greeting); // Hello, Rahul Sharma!
What you CANNOT do:
let firstName: string = "Rahul";
firstName = 42; // ERROR: Type 'number' is not assignable to type 'string'
firstName = true; // ERROR: Type 'boolean' is not assignable to type 'string'
2.2 — number
Represents any numeric value — integers, decimals, negative numbers. Unlike some languages (Java, C++), TypeScript doesn't have separate int and float types. Everything is just number.
let age: number = 25;
let price: number = 999.99;
let temperature: number = -5;
let bigNumber: number = 1_000_000; // underscores for readability — still valid
let hex: number = 0xff; // hexadecimal
let binary: number = 0b1010; // binary
console.log(age, price, temperature); // 25 999.99 -5
2.3 — boolean
Represents true or false. That's it. Nothing else.
let isLoggedIn: boolean = true;
let hasSubscription: boolean = false;
let isAdmin: boolean = false;
if (isLoggedIn && hasSubscription) {
console.log("Show premium content");
} else if (isLoggedIn && !hasSubscription) {
console.log("Show free content");
} else {
console.log("Please login");
}
Output:
Show free content
2.4 — null and undefined
These two are different and understanding the difference matters.
undefined = a variable has been declared but not assigned a value yet.
null = a variable has been intentionally set to "no value".
let userAddress: string | null = null;
// The user exists but hasn't provided their address yet.
// We're explicitly saying: this field exists but is empty.
let draftTitle: string | undefined = undefined;
// This hasn't been set yet at all.
The string | null syntax is called a Union Type. We'll cover this properly in Chapter 6. For now just read it as "this can be a string OR null".
2.5 — any
any is TypeScript's escape hatch. A variable of type any can hold any value, just like regular JavaScript.
let data: any = 5;
data = "hello"; // no error
data = true; // no error
data = { x: 1 }; // no error
Use any sparingly. When you use any, you're essentially turning off TypeScript's protection for that variable. It's sometimes necessary (like when working with third-party libraries that don't have TypeScript support), but don't make a habit of it.
If you find yourself writing any everywhere, you're not actually writing TypeScript — you're writing JavaScript with extra steps.
2.6 — unknown
unknown is the safer alternative to any. A variable of type unknown can hold any value, but before you can use it, you have to check what type it actually is.
let input: unknown = "Hello World";
// You CANNOT do this:
// console.log(input.toUpperCase()); // ERROR: Object is of type 'unknown'
// You MUST check the type first:
if (typeof input === "string") {
console.log(input.toUpperCase()); // Now this works: "HELLO WORLD"
}
Use unknown instead of any when you genuinely don't know the type ahead of time. It's safer because TypeScript forces you to verify the type before using it.
2.7 — never
never represents a value that never occurs. This is used for functions that never return (they always throw an error or run forever).
function throwError(message: string): never {
throw new Error(message);
// This function never returns normally, so return type is 'never'
}
function infiniteLoop(): never {
while (true) {
// runs forever
}
}
You won't use never much as a beginner, but you'll encounter it in error messages sometimes.
Chapter 3 — Type Inference — TypeScript is Smart
Here's something important: you don't always need to write the type explicitly. TypeScript can figure it out on its own.
let name = "Rahul"; // TypeScript infers: name is string
let age = 25; // TypeScript infers: age is number
let isActive = true; // TypeScript infers: isActive is boolean
If you hover over these variables in VS Code, it will show you the inferred type.
This means you don't always need to write:
let name: string = "Rahul";
You can just write:
let name = "Rahul";
And TypeScript will still give you an error if you try to assign a number to it later.
When should you write the type explicitly?
- When declaring a variable without assigning a value yet:
let result: string;
- When the inferred type isn't specific enough
- In function parameters and return types — always be explicit here for clarity
Chapter 4 — Arrays and Tuples
4.1 — Arrays
There are two ways to type an array:
// Method 1 — using brackets notation (more common)
let fruits: string[] = ["apple", "mango", "banana"];
let scores: number[] = [95, 87, 72, 100];
let flags: boolean[] = [true, false, true];
// Method 2 — using generic notation
let fruits2: Array<string> = ["apple", "mango"];
let scores2: Array<number> = [95, 87];
Both are valid. Use whichever feels more readable to you. Most Angular developers use Method 1.
Working with arrays:
let products: string[] = ["Laptop", "Phone", "Tablet"];
// Add to end
products.push("Keyboard");
console.log(products); // ["Laptop", "Phone", "Tablet", "Keyboard"]
// Remove from end
products.pop();
console.log(products); // ["Laptop", "Phone", "Tablet"]
// Find an item
let index = products.indexOf("Phone");
console.log(index); // 1
// Filter items
let longNames = products.filter(p => p.length > 5);
console.log(longNames); // ["Laptop", "Tablet"]
// Map (transform) items
let upperProducts = products.map(p => p.toUpperCase());
console.log(upperProducts); // ["LAPTOP", "PHONE", "TABLET"]
// Find one item
let found = products.find(p => p === "Phone");
console.log(found); // "Phone"
// Check if item exists
let exists = products.includes("Laptop");
console.log(exists); // true
These array methods are used CONSTANTLY in Angular. Especially filter, map, and find. Know them deeply.
4.2 — Tuples
Tuples in TypeScript are fixed-length arrays where each position has a specific, known type.
Basic Syntax
let point: [number, number] = [10, 20];
let entry: [string, number] = ["Alice", 30];
The order and types are enforced — swapping them causes a compile error.
Why Use Tuples?
Regular arrays are homogeneous (number[], string[]). Tuples let you group different types together with positional meaning, without needing a full object.
Common Use Cases
1. Returning multiple values from a function
function getMinMax(nums: number[]): [number, number] {
return [Math.min(...nums), Math.max(...nums)];
}
const [min, max] = getMinMax([3, 1, 4, 1, 5]);
2. Key-value pairs
const pair: [string, number] = ["age", 25];
3. React-style hooks (this is exactly how useState is typed internally)
function useState<T>(initial: T): [T, (val: T) => void] { ... }
const [count, setCount] = useState(0);
Optional & Rest Elements
// Optional element
let rgb: [number, number, number, number?] = [255, 0, 128]; // alpha is optional
// Rest elements (variable length tail)
let log: [string, ...number[]] = ["scores", 90, 85, 78];
Named Tuple Elements (TS 4.0+)
You can label positions for better readability — labels are purely documentary and don't affect behavior:
type Range = [start: number, end: number];
function getRange(): Range {
return [0, 100];
}
Readonly Tuples
Prevent mutation with readonly:
const point: readonly [number, number] = [10, 20];
point[0] = 5; // ❌ Error: Cannot assign to '0' because it is a read-only
Tuples vs Arrays
|
Array
|
Tuple
|
|
Length
|
Variable
|
Fixed
|
|
Types
|
Homogeneous
|
Heterogeneous
|
|
Access
|
By index
|
By index
|
|
Use when
|
Same-type list
|
Small, ordered group
|
Key Gotcha
TypeScript doesn't enforce tuple length at runtime — it's a compile-time feature only. If you push to a tuple, TypeScript won't always catch it unless you use readonly.
let pair: [string, number] = ["hello", 1];
pair.push(999); // TS allows this — be careful!
console.log(pair); // ["hello", 1, 999]
In short, use tuples when you have a small, ordered, fixed collection of values with different types and positional meaning matters.
Real-world use in Angular — the useState-style approach:
// HTTP response often comes as [data, error]
let apiResponse: [any, string | null] = [{ users: [] }, null];
Chapter 5 — Objects and Type Aliases
5.1 — Typing Objects Inline
let user: { name: string; age: number; email: string } = {
name: "Priya",
age: 28,
email: "priya@gmail.com"
};
This works but it's messy. If you need this shape in multiple places, you'd have to repeat this big type annotation everywhere. This is where Type Aliases come in.
5.2 — Type Aliases
A type alias creates a reusable name for a type.
type User = {
name: string;
age: number;
email: string;
};
let user1: User = {
name: "Priya",
age: 28,
email: "priya@gmail.com"
};
let user2: User = {
name: "Rahul",
age: 25,
email: "rahul@gmail.com"
};
// Now you can reuse 'User' anywhere
function printUser(user: User): void {
console.log(`${user.name} (${user.age}) — ${user.email}`);
}
printUser(user1); // Priya (28) — priya@gmail.com
printUser(user2); // Rahul (25) — rahul@gmail.com
5.3 — Optional Properties
Sometimes a property may or may not exist. Use ? to mark it as optional.
type Product = {
id: number;
name: string;
price: number;
description?: string; // optional — may or may not exist
discount?: number; // optional
};
// This is valid even without description and discount
let laptop: Product = {
id: 1,
name: "Laptop",
price: 55000
};
// This is also valid
let phone: Product = {
id: 2,
name: "iPhone",
price: 80000,
description: "Latest model with great camera",
discount: 5000
};
5.4 — Readonly Properties
Sometimes you want a property that can be set once but never changed.
type Config = {
readonly apiUrl: string;
readonly maxRetries: number;
timeout: number; // this one can change
};
let appConfig: Config = {
apiUrl: "https://api.myapp.com",
maxRetries: 3,
timeout: 5000
};
appConfig.timeout = 10000; // OK — timeout is not readonly
appConfig.apiUrl = "http://other"; // ERROR: Cannot assign to 'apiUrl' because it is a read-only property
Chapter 6 — Interfaces — The Angular Developer's Best Friend
Interfaces are similar to Type Aliases but they work differently under the hood and are more powerful for object shapes.
Here's the key practical difference:
- Type Aliases can describe any type: objects, unions, primitives, tuples
- Interfaces are specifically for describing the shape of objects (and classes)
In Angular codebases, you'll see both. But interfaces are more common for defining data shapes that come from APIs.
6.1 — Basic Interface
interface Product {
id: number;
title: string;
price: number;
category: string;
inStock: boolean;
image?: string; // optional
}
let product1: Product = {
id: 1,
title: "MacBook Pro",
price: 150000,
category: "Electronics",
inStock: true
};
let product2: Product = {
id: 2,
title: "iPhone 15",
price: 80000,
category: "Electronics",
inStock: false,
image: "iphone15.jpg"
};
6.2 — Interface with Methods
An interface can also define methods that any implementing object must have.
interface Vehicle {
brand: string;
speed: number;
accelerate(amount: number): void;
brake(amount: number): void;
getInfo(): string;
}
6.3 — Interface Extending Another Interface
This is where interfaces shine over type aliases — clean inheritance.
interface Animal {
name: string;
age: number;
}
interface Dog extends Animal {
breed: string;
canFetch: boolean;
}
// Dog now has: name, age, breed, canFetch
let myDog: Dog = {
name: "Bruno",
age: 3,
breed: "Labrador",
canFetch: true
};
Real-world Angular example:
interface BaseEntity {
id: number;
createdAt: string;
updatedAt: string;
}
interface User extends BaseEntity {
name: string;
email: string;
role: "admin" | "user" | "guest";
}
interface BlogPost extends BaseEntity {
title: string;
content: string;
authorId: number;
tags: string[];
}
Now User and BlogPost both automatically have id, createdAt, and updatedAt because they extend BaseEntity. This is exactly the pattern you'll use when your API returns data.
6.4 — Interface vs Type — When to Use Which
For objects that represent data models (API responses, database entities): use Interface
interface UserResponse {
id: number;
name: string;
email: string;
}
For unions, tuples, or complex type combinations: use Type
type Status = "loading" | "success" | "error";
type ID = string | number;
type Pair = [string, number];
Chapter 7 — Union and Intersection Types
7.1 — Union Types — OR
A union type means a value can be one type OR another type.
type StringOrNumber = string | number;
let id: StringOrNumber = 5;
id = "abc-123"; // also valid
id = true; // ERROR: boolean is not in the union
// Practical example
type Status = "active" | "inactive" | "banned";
let userStatus: Status = "active";
userStatus = "inactive"; // valid
userStatus = "deleted"; // ERROR: "deleted" is not in the union
The "active" | "inactive" | "banned" is called a String Literal Union. This is extremely common in Angular. You're not just saying "any string" — you're saying "exactly one of these specific strings".
// Without union — accepts ANY string
function setStatus(status: string) { ... }
setStatus("anything goes"); // no error even if wrong
// With union — only specific strings allowed
function setStatus(status: "active" | "inactive" | "banned") { ... }
setStatus("active"); // valid
setStatus("deleted"); // ERROR caught at compile time
7.2 — Intersection Types — AND
An intersection type combines multiple types into one. The result must have ALL properties of all combined types.
type Timestamps = {
createdAt: string;
updatedAt: string;
};
type UserData = {
id: number;
name: string;
email: string;
};
// UserWithTimestamps has ALL properties from both
type UserWithTimestamps = UserData & Timestamps;
let user: UserWithTimestamps = {
id: 1,
name: "Rahul",
email: "rahul@gmail.com",
createdAt: "2024-01-01",
updatedAt: "2024-03-15"
};
Chapter 8 — Functions — Deep Dive
Functions are where you'll spend most of your time in Angular. Let's cover everything.
8.1 — Function Parameters and Return Types
// Basic function with typed params and return type
function add(a: number, b: number): number {
return a + b;
}
// Arrow function version
const multiply = (a: number, b: number): number => a * b;
// Function that returns nothing
function logError(message: string): void {
console.error(`[ERROR]: ${message}`);
}
// Function with optional parameter
function greet(name: string, title?: string): string {
if (title) {
return `Hello, ${title} ${name}!`;
}
return `Hello, ${name}!`;
}
console.log(greet("Sharma")); // Hello, Sharma!
console.log(greet("Sharma", "Dr.")); // Hello, Dr. Sharma!
8.2 — Default Parameters
function createUser(
name: string,
role: string = "user", // default value
isActive: boolean = true // default value
): void {
console.log(`Created: ${name}, Role: ${role}, Active: ${isActive}`);
}
createUser("Rahul"); // Created: Rahul, Role: user, Active: true
createUser("Priya", "admin"); // Created: Priya, Role: admin, Active: true
createUser("Amit", "user", false); // Created: Amit, Role: user, Active: false
8.3 — Rest Parameters
When you don't know how many arguments will be passed:
function sum(...numbers: number[]): number {
return numbers.reduce((total, n) => total + n, 0);
}
console.log(sum(1, 2, 3)); // 6
console.log(sum(10, 20, 30, 40, 50)); // 150
8.4 — Function Types — Functions as Values
In Angular, you'll sometimes pass functions as arguments (callbacks, event handlers). You can type these too.
// Define a type for a function
type MathOperation = (a: number, b: number) => number;
// Use that type
let add: MathOperation = (a, b) => a + b;
let subtract: MathOperation = (a, b) => a - b;
let multiply: MathOperation = (a, b) => a * b;
function calculate(a: number, b: number, operation: MathOperation): number {
return operation(a, b);
}
console.log(calculate(10, 5, add)); // 15
console.log(calculate(10, 5, subtract)); // 5
console.log(calculate(10, 5, multiply)); // 50
8.5 — Async Functions
You'll use these constantly when fetching API data in Angular services.
// A function that returns a Promise of a number
async function fetchUserId(name: string): Promise<number> {
// Simulating an API call delay
await new Promise(resolve => setTimeout(resolve, 1000));
return 42; // returning a user ID
}
// Calling it
async function main() {
const userId = await fetchUserId("Rahul");
console.log("User ID:", userId); // User ID: 42
}
main();
Chapter 9 — Classes — The Heart of Angular
Every single Angular component, service, module, pipe, and guard is a class. Understanding TypeScript classes deeply means understanding Angular deeply.
9.1 — Basic Class Structure
class BankAccount {
// Properties
accountNumber: string;
holderName: string;
balance: number;
// Constructor — runs when you create a new instance
constructor(accountNumber: string, holderName: string, initialBalance: number) {
this.accountNumber = accountNumber;
this.holderName = holderName;
this.balance = initialBalance;
}
// Methods
deposit(amount: number): void {
if (amount <= 0) {
console.log("Deposit amount must be positive");
return;
}
this.balance += amount;
console.log(`Deposited ₹${amount}. New balance: ₹${this.balance}`);
}
withdraw(amount: number): void {
if (amount > this.balance) {
console.log("Insufficient funds");
return;
}
this.balance -= amount;
console.log(`Withdrew ₹${amount}. New balance: ₹${this.balance}`);
}
getStatement(): string {
return `Account: ${this.accountNumber} | Holder: ${this.holderName} | Balance: ₹${this.balance}`;
}
}
// Create instances
let account1 = new BankAccount("ACC001", "Rahul Sharma", 10000);
let account2 = new BankAccount("ACC002", "Priya Patel", 25000);
account1.deposit(5000); // Deposited ₹5000. New balance: ₹15000
account1.withdraw(2000); // Withdrew ₹2000. New balance: ₹13000
console.log(account1.getStatement());
// Account: ACC001 | Holder: Rahul Sharma | Balance: ₹13000
9.2 — Access Modifiers — public, private, protected
This is critical for Angular because you'll use access modifiers in every component and service.
class Employee {
public name: string; // accessible from anywhere
public department: string;
private salary: number; // only accessible inside this class
private ssn: string; // sensitive data — keep private
protected employeeId: string; // accessible here and in subclasses
constructor(
name: string,
department: string,
salary: number,
ssn: string,
employeeId: string
) {
this.name = name;
this.department = department;
this.salary = salary;
this.ssn = ssn;
this.employeeId = employeeId;
}
// A public method that safely exposes salary info
getSalaryInfo(): string {
return `${this.name} earns in the ${this.getSalaryRange()} range`;
}
// A private helper method — only used internally
private getSalaryRange(): string {
if (this.salary < 30000) return "junior";
if (this.salary < 70000) return "mid-level";
return "senior";
}
}
let emp = new Employee("Amit", "Engineering", 65000, "XXX-XX-1234", "EMP001");
console.log(emp.name); // "Amit" — works, public
console.log(emp.getSalaryInfo()); // works, public method
console.log(emp.salary); // ERROR: 'salary' is private
9.3 — Shorthand Constructor (Used Constantly in Angular)
TypeScript gives you a shorthand to declare and initialize properties directly in the constructor:
// Long way:
class User {
name: string;
email: string;
role: string;
constructor(name: string, email: string, role: string) {
this.name = name;
this.email = email;
this.role = role;
}
}
// Shorthand way (EXACT same result):
class User {
constructor(
public name: string,
public email: string,
private role: string
) { } // constructor body is empty — TypeScript handles it automatically
}
You will see this shorthand in Angular services ALL the time:
// Angular service injection shorthand
constructor(
private http: HttpClient,
private router: Router,
private authService: AuthService
) { }
That one line declares http, router, and authService as private properties AND injects them. This is the standard Angular pattern.
9.4 — Inheritance — extends
class Animal {
name: string;
sound: string;
constructor(name: string, sound: string) {
this.name = name;
this.sound = sound;
}
makeSound(): void {
console.log(`${this.name} says ${this.sound}`);
}
describe(): string {
return `I am ${this.name}`;
}
}
class Dog extends Animal {
breed: string;
constructor(name: string, breed: string) {
super(name, "Woof"); // super() calls the parent constructor
this.breed = breed;
}
fetch(item: string): void {
console.log(`${this.name} fetched the ${item}!`);
}
// Override parent method
describe(): string {
return `${super.describe()} and I am a ${this.breed}`;
}
}
let dog = new Dog("Bruno", "Labrador");
dog.makeSound(); // Bruno says Woof
dog.fetch("ball"); // Bruno fetched the ball!
console.log(dog.describe()); // I am Bruno and I am a Labrador
9.5 — Abstract Classes
An abstract class is a class that cannot be instantiated directly. It serves as a template for other classes.
abstract class Shape {
color: string;
constructor(color: string) {
this.color = color;
}
// Abstract method — subclasses MUST implement this
abstract calculateArea(): number;
// Regular method — subclasses inherit this as-is
describe(): void {
console.log(`This is a ${this.color} shape with area ${this.calculateArea()}`);
}
}
class Circle extends Shape {
radius: number;
constructor(color: string, radius: number) {
super(color);
this.radius = radius;
}
calculateArea(): number { // must implement abstract method
return Math.PI * this.radius * this.radius;
}
}
class Rectangle extends Shape {
width: number;
height: number;
constructor(color: string, width: number, height: number) {
super(color);
this.width = width;
this.height = height;
}
calculateArea(): number { // must implement abstract method
return this.width * this.height;
}
}
let circle = new Circle("red", 5);
circle.describe(); // This is a red shape with area 78.53...
let rect = new Rectangle("blue", 10, 4);
rect.describe(); // This is a blue shape with area 40
// let shape = new Shape("green"); // ERROR: Cannot instantiate abstract class
Chapter 10 — Generics — Writing Flexible Reusable Code
Generics are one of the most powerful TypeScript features and they appear everywhere in Angular, especially with HTTP calls and RxJS.
10.1 — The Problem Generics Solve
Imagine you want to write a function that returns the first item of an array. Without generics:
function getFirstItem(arr: string[]): string {
return arr[0];
}
// Only works for string arrays. What about number arrays? object arrays?
You'd have to write the same function multiple times for each type. OR use any, which loses type safety.
With Generics:
function getFirstItem<T>(arr: T[]): T {
return arr[0];
}
// TypeScript infers the type automatically
let firstFruit = getFirstItem(["apple", "mango", "banana"]);
// TypeScript knows firstFruit is a string
let firstScore = getFirstItem([95, 87, 72]);
// TypeScript knows firstScore is a number
let firstUser = getFirstItem([{ id: 1, name: "Rahul" }, { id: 2, name: "Priya" }]);
// TypeScript knows firstUser is { id: number, name: string }
<T> is a type parameter. It's a placeholder for whatever type gets passed in. The name T is convention (stands for Type), but you can use any name.
10.2 — Generic Interfaces — The Angular HTTP Pattern
This is exactly how Angular's HTTP client works:
// Generic API response wrapper
interface ApiResponse<T> {
data: T;
status: number;
message: string;
timestamp: string;
}
// Specific interfaces for different data
interface User {
id: number;
name: string;
email: string;
}
interface Product {
id: number;
title: string;
price: number;
}
// Now use the generic wrapper with specific types
let userResponse: ApiResponse<User> = {
data: { id: 1, name: "Rahul", email: "rahul@gmail.com" },
status: 200,
message: "Success",
timestamp: "2024-03-28T10:00:00Z"
};
let productResponse: ApiResponse<Product> = {
data: { id: 1, title: "Laptop", price: 55000 },
status: 200,
message: "Success",
timestamp: "2024-03-28T10:00:00Z"
};
// ApiResponse<User[]> for a list of users
let usersResponse: ApiResponse<User[]> = {
data: [
{ id: 1, name: "Rahul", email: "rahul@gmail.com" },
{ id: 2, name: "Priya", email: "priya@gmail.com" }
],
status: 200,
message: "Success",
timestamp: "2024-03-28T10:00:00Z"
};
In Angular's HTTP client, you write this.http.get<User[]>(url). That <User[]> is a generic — you're telling TypeScript what shape the API response data will be.
10.3 — Generic Classes
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
peek(): T | undefined {
return this.items[this.items.length - 1];
}
isEmpty(): boolean {
return this.items.length === 0;
}
size(): number {
return this.items.length;
}
}
// A stack of numbers
let numberStack = new Stack<number>();
numberStack.push(10);
numberStack.push(20);
numberStack.push(30);
console.log(numberStack.peek()); // 30
console.log(numberStack.pop()); // 30
console.log(numberStack.size()); // 2
// A stack of strings — same class, different type
let stringStack = new Stack<string>();
stringStack.push("first");
stringStack.push("second");
console.log(stringStack.pop()); // "second"
Chapter 11 — Enums — Named Constants
Enums let you define a set of named constants. Instead of using magic strings or numbers throughout your code, you give them meaningful names.
// Without enum — bad practice
let userRole = "admin"; // What are the valid values? Hard to know.
let orderStatus = 2; // What does 2 mean? Hard to know.
// With enum — clear and safe
enum UserRole {
Admin = "admin",
User = "user",
Guest = "guest",
Moderator = "moderator"
}
enum OrderStatus {
Pending = "PENDING",
Processing = "PROCESSING",
Shipped = "SHIPPED",
Delivered = "DELIVERED",
Cancelled = "CANCELLED"
}
// Using enums
let currentUserRole: UserRole = UserRole.Admin;
let currentOrderStatus: OrderStatus = OrderStatus.Processing;
console.log(currentUserRole); // "admin"
console.log(currentOrderStatus); // "PROCESSING"
// Excellent for switch statements
function getStatusMessage(status: OrderStatus): string {
switch (status) {
case OrderStatus.Pending:
return "Your order is waiting to be processed";
case OrderStatus.Processing:
return "Your order is being prepared";
case OrderStatus.Shipped:
return "Your order is on its way";
case OrderStatus.Delivered:
return "Your order has been delivered";
case OrderStatus.Cancelled:
return "Your order has been cancelled";
default:
return "Unknown status";
}
}
console.log(getStatusMessage(OrderStatus.Shipped));
// "Your order is on its way"
Chapter 12 — Type Guards and Narrowing
Type narrowing is the process of refining a type within a conditional block. TypeScript is smart enough to understand that after a type check, the type is more specific.
type StringOrNumber = string | number;
function processValue(value: StringOrNumber): string {
// At this point, 'value' could be string or number
if (typeof value === "string") {
// Inside here, TypeScript KNOWS value is a string
return value.toUpperCase(); // .toUpperCase() is valid
} else {
// Inside here, TypeScript KNOWS value is a number
return value.toFixed(2); // .toFixed() is valid
}
}
console.log(processValue("hello")); // "HELLO"
console.log(processValue(42.567)); // "42.57"
With interfaces:
interface Cat {
name: string;
meow(): void;
}
interface Dog {
name: string;
bark(): void;
}
type Pet = Cat | Dog;
// Type guard function
function isCat(pet: Pet): pet is Cat {
return "meow" in pet;
}
function makeNoise(pet: Pet): void {
if (isCat(pet)) {
pet.meow(); // TypeScript knows it's a Cat here
} else {
pet.bark(); // TypeScript knows it's a Dog here
}
}
Chapter 13 — Decorators (Angular's Secret Sauce)
Decorators are a special TypeScript feature that Angular uses for absolutely everything. Let's understand them deeply.
A decorator is a function that is applied to a class, method, property, or parameter using the @ symbol. It adds metadata or modifies behavior.
13.1 — Class Decorator
// This is a decorator factory — a function that returns a decorator
function Component(config: { selector: string; template: string }) {
return function (constructor: Function) {
// Attach metadata to the class
constructor.prototype.selector = config.selector;
constructor.prototype.template = config.template;
console.log(`Component registered: ${config.selector}`);
}
}
// Using the decorator
@Component({
selector: 'app-header',
template: '<header>My App</header>'
})
class HeaderComponent {
// class code
}
// Console: "Component registered: app-header"
This is literally what Angular's @Component() decorator does — it takes your class and registers it as an Angular component with the selector and template metadata.
13.2 — Property Decorator
function Log(target: any, propertyKey: string) {
let value = target[propertyKey];
Object.defineProperty(target, propertyKey, {
get: () => {
console.log(`Getting ${propertyKey}: ${value}`);
return value;
},
set: (newValue) => {
console.log(`Setting ${propertyKey} to: ${newValue}`);
value = newValue;
}
});
}
class User {
@Log
name: string = "Rahul";
}
let user = new User();
user.name = "Priya"; // Console: "Setting name to: Priya"
console.log(user.name); // Console: "Getting name: Priya"
Angular's @Input() and @Output() work on this same principle — they're property decorators.
Chapter 14 — Modules and Imports/Exports
This is fundamental to how Angular is organized. Every Angular project is a set of TypeScript modules importing and exporting things from each other.
// math-utils.ts
export function add(a: number, b: number): number {
return a + b;
}
export function subtract(a: number, b: number): number {
return a - b;
}
export const PI = 3.14159265;
// Default export — only one per file
export default function multiply(a: number, b: number): number {
return a * b;
}
// main.ts
import multiply, { add, subtract, PI } from './math-utils';
// multiply is the default export, add/subtract/PI are named exports
console.log(add(5, 3)); // 8
console.log(subtract(10, 4)); // 6
console.log(PI); // 3.14159265
console.log(multiply(3, 4)); // 12
// Import everything as a namespace
import * as MathUtils from './math-utils';
console.log(MathUtils.add(2, 2)); // 4
In Angular, every component, service, module, and interface is exported from its file and imported where needed. This module system is how Angular organizes code.
Phase 1 Complete — You're TypeScript Ready
Let's recap everything we covered:
Type System — Why TypeScript exists, static type checking, catching bugs before runtime
Primitive Types — string, number, boolean, null, undefined, any, unknown, never — all 7 with real use cases
Type Inference — TypeScript figuring out types automatically
Arrays and Tuples — Typed arrays, array methods (map, filter, find, reduce), fixed-length tuples
Objects and Type Aliases — Inline object types, reusable type aliases, optional and readonly properties
Interfaces — Object blueprints, extending interfaces, interface vs type
Union and Intersection Types — OR types, AND types, string literal unions
Functions — Parameters, return types, optional params, default params, rest params, function types, async functions
Classes — Properties, constructors, methods, access modifiers (public/private/protected), shorthand constructors, inheritance with extends, super(), abstract classes
Generics — Generic functions, generic interfaces, generic classes — the foundation for Angular's HTTP calls
Enums — Named constants for roles, statuses, and categories
Type Guards — Narrowing types inside conditionals, custom type guard functions
Decorators — Class, property, and method decorators — exactly how Angular uses them
Modules — Export, import, default exports, named exports
One Final Practice Exercise Before Angular
Write this from scratch to test your understanding:
Build a simple Library system:
- Create an interface
Book with id, title, author, year, genre, and optional rating
- Create an enum
Genre with at least 5 genres
- Create a class
Library with a private array of books
- Add methods:
addBook, removeBook, findByAuthor, findByGenre, getTopRated
- The
getTopRated method should accept a minimum rating and return all books above it
- Use proper TypeScript types everywhere — no
any
If you can build this comfortably, you are ready for Angular Phase 2.