JavaScript + Backend Bootcamp
Day 8 : Advanced TypeScript
Today we move beyond basic TypeScript and learn the features used in production applications.
You’ll learn:
- Advanced union types
- Intersection types
- Type narrowing
- Type guards
- Generics
- Generic constraints
- Classes
- Access modifiers
- Inheritance
- Abstract classes
- Utility types
keyof- Indexed access types
- Async TypeScript patterns
- Safer API typing
- TypeScript best practices
Recommended Reading 📖
Review this article before starting:
From JavaScript to TypeScript: How TypeScript Improves Code Quality
It explains the transition from JavaScript to TypeScript and the importance of type safety.
Day 8 builds on those foundations with advanced and backend-focused TypeScript concepts.
1. Union Types
A union allows a value to have more than one possible type.
let userId: string | number;userId = 101;
userId = "USER-101";Function example:
function printId(id: string | number): void {
console.log(`User ID: ${id}`);
}printId(101);
printId("USER-101");2. Type Narrowing
When a value has multiple possible types, TypeScript requires you to check its actual type before using type-specific methods.
function formatValue(value: string | number): string {
if (typeof value === "string") {
return value.toUpperCase();
}return value.toFixed(2);
}console.log(formatValue("typescript"));
console.log(formatValue(95.567));
Output:
TYPESCRIPT
95.573. Literal Types
Literal types restrict values to specific choices.
type UserRole = "admin" | "editor" | "user";let role: UserRole = "admin";This produces an error:
role = "manager";Backend example:
type HttpMethod =
| "GET"
| "POST"
| "PUT"
| "PATCH"
| "DELETE";function sendRequest(method: HttpMethod): void {
console.log(`Sending ${method} request`);
}sendRequest("POST");
4. Discriminated Unions
Discriminated unions help TypeScript identify different object structures safely.
type SuccessResponse = {
status: "success";
data: string[];
};type ErrorResponse = {
status: "error";
message: string;
};type ApiResponse = SuccessResponse | ErrorResponse;
Use it safely:
function handleResponse(response: ApiResponse): void {
if (response.status === "success") {
console.log(response.data);
} else {
console.log(response.message);
}
}This pattern is useful for:
- API responses
- Application states
- Authentication results
- Payment status
- Database operations
5. Intersection Types
Intersection types combine multiple types.
type Person = {
name: string;
age: number;
};type Employee = {
employeeId: number;
department: string;
};type StaffMember = Person & Employee;
Example:
const employee: StaffMember = {
name: "Ravi",
age: 22,
employeeId: 101,
department: "Engineering"
};console.log(employee);6. Type Guards
Type guards help identify a value’s type.
Using typeof
function display(value: string | number): void {
if (typeof value === "string") {
console.log(value.toUpperCase());
} else {
console.log(value.toFixed(2));
}
}Using in
type Admin = {
name: string;
permissions: string[];
};type Customer = {
name: string;
orders: number;
};function showUser(user: Admin | Customer): void {
if ("permissions" in user) {
console.log(user.permissions);
} else {
console.log(user.orders);
}
}
Using instanceof
class AppError extends Error {
statusCode: number;constructor(message: string, statusCode: number) {
super(message);
this.statusCode = statusCode;
}
}function handleError(error: Error | AppError): void {
if (error instanceof AppError) {
console.log(error.statusCode);
} console.log(error.message);
}
7. Custom Type Guards
You can create a function that tells TypeScript which type a value has.
interface User {
id: number;
name: string;
}function isUser(value: unknown): value is User {
if (typeof value !== "object" || value === null) {
return false;
} return (
"id" in value &&
typeof value.id === "number" &&
"name" in value &&
typeof value.name === "string"
);
}
Usage:
const data: unknown = {
id: 1,
name: "Ravi"
};if (isUser(data)) {
console.log(data.name);
}Custom type guards are useful when working with:
- API data
- JSON
- User input
- Environment variables
- Third-party packages
8. Generics
Generics allow reusable code while preserving type safety.
Get Bhaskar Chowdary’s stories in your inbox
Join Medium for free to get updates from this writer.
Without generics:
function getValue(value: any): any {
return value;
}With generics:
function getValue<T>(value: T): T {
return value;
}const username = getValue<string>("Ravi");
const age = getValue<number>(22);TypeScript can usually infer the type:
const city = getValue("Hyderabad");
const marks = getValue(95);9. Generic Arrays
function getFirstItem<T>(items: T[]): T | undefined {
return items[0];
}const firstNumber = getFirstItem([10, 20, 30]);
const firstFruit = getFirstItem([
"Apple",
"Banana",
"Mango"
]);console.log(firstNumber);
console.log(firstFruit);
10. Generic Interfaces
interface ApiResponse<T> {
success: boolean;
message: string;
data: T;
}User response:
interface User {
id: number;
name: string;
email: string;
}const userResponse: ApiResponse<User> = {
success: true,
message: "User fetched successfully",
data: {
id: 1,
name: "Ravi",
email: "ravi@example.com"
}
};Array response:
const usersResponse: ApiResponse<User[]> = {
success: true,
message: "Users fetched successfully",
data: [
{
id: 1,
name: "Ravi",
email: "ravi@example.com"
}
]
};This pattern will be heavily used in backend APIs.
11. Generic Constraints
Generic constraints limit which types can be passed.
function printLength<T extends { length: number }>(
value: T
): number {
return value.length;
}console.log(printLength("TypeScript"));
console.log(printLength([1, 2, 3]));This produces an error:
printLength(100);A number does not have a length property.
12. Multiple Generic Types
function createPair<T, U>(
first: T,
second: U
): [T, U] {
return [first, second];
}const pair = createPair("Ravi", 22);console.log(pair);
The inferred type is:
[string, number]13. keyof Operator
keyof creates a union of an object type’s property names.
interface User {
id: number;
name: string;
email: string;
}type UserKey = keyof User;UserKey becomes:
"id" | "name" | "email"Example:
function getProperty<T, K extends keyof T>(
object: T,
key: K
): T[K] {
return object[key];
}const user: User = {
id: 1,
name: "Ravi",
email: "ravi@example.com"
};console.log(getProperty(user, "name"));
This produces an error:
getProperty(user, "password");14. typeof in Type Positions
TypeScript can create a type from an existing value.
const defaultConfig = {
port: 3000,
environment: "development",
logging: true
};type AppConfig = typeof defaultConfig;Now:
const productionConfig: AppConfig = {
port: 8080,
environment: "production",
logging: false
};15. Indexed Access Types
You can access the type of a specific property.
interface Product {
id: number;
name: string;
price: number;
}type ProductPrice = Product["price"];ProductPrice is:
numberMultiple properties:
type ProductInfo = Product["name" | "price"];ProductInfo becomes:
string | number16. Classes in TypeScript
class User {
name: string;
age: number;constructor(name: string, age: number) {
this.name = name;
this.age = age;
} greet(): string {
return `Hello, my name is ${this.name}`;
}
}const user = new User("Ravi", 22);console.log(user.greet());
17. Constructor Parameter Properties
TypeScript provides a shorter class syntax.
class Product {
constructor(
public name: string,
public price: number
) {}display(): string {
return `${this.name}: ₹${this.price}`;
}
}const product = new Product("Keyboard", 1500);console.log(product.display());
TypeScript automatically creates and assigns the properties.
18. Access Modifiers
TypeScript supports:
publicprivateprotected
Public
Public members can be accessed anywhere.
class User {
constructor(public name: string) {}
}const user = new User("Ravi");console.log(user.name);
public is the default.
Private
Private members can only be accessed inside the class.
class BankAccount {
private balance = 0;deposit(amount: number): void {
if (amount <= 0) {
throw new Error(
"Deposit amount must be positive"
);
} this.balance += amount;
} getBalance(): number {
return this.balance;
}
}const account = new BankAccount();account.deposit(1000);console.log(account.getBalance());
This produces an error:
console.log(account.balance);Protected
Protected members can be accessed inside the class and its child classes.
class Employee {
constructor(
protected salary: number
) {}
}class Manager extends Employee {
showSalary(): number {
return this.salary;
}
}const manager = new Manager(50000);console.log(manager.showSalary());
19. Readonly Class Properties
class User {
constructor(
public readonly id: number,
public name: string
) {}
}const user = new User(1, "Ravi");user.name = "Arun";
This produces an error:
user.id = 2;20. Getters and Setters
class Employee {
private _salary = 0;get salary(): number {
return this._salary;
} set salary(value: number) {
if (value < 0) {
throw new Error(
"Salary cannot be negative"
);
} this._salary = value;
}
}const employee = new Employee();employee.salary = 50000;console.log(employee.salary);
21. Inheritance
Inheritance allows one class to reuse another class.
class Person {
constructor(public name: string) {}greet(): string {
return `Hello, I am ${this.name}`;
}
}class Admin extends Person {
constructor(
name: string,
public permissions: string[]
) {
super(name);
} showPermissions(): string[] {
return this.permissions;
}
}const admin = new Admin(
"Ravi",
["create", "update", "delete"]
);console.log(admin.greet());
console.log(admin.showPermissions());
22. Method Overriding
A child class can replace a parent method.
class User {
getRole(): string {
return "User";
}
}class Admin extends User {
override getRole(): string {
return "Admin";
}
}const admin = new Admin();console.log(admin.getRole());
23. Abstract Classes
An abstract class cannot be instantiated directly.
abstract class Payment {
constructor(public amount: number) {}abstract process(): string; displayAmount(): string {
return `Amount: ₹${this.amount}`;
}
}
Child class:
class CardPayment extends Payment {
process(): string {
return `Card payment of ₹${this.amount} processed`;
}
}const payment = new CardPayment(2000);console.log(payment.process());
This is not allowed:
const payment = new Payment(2000);24. Implementing Interfaces
Classes can implement interfaces.
interface Authenticatable {
login(): boolean;
logout(): void;
}class AdminUser implements Authenticatable {
login(): boolean {
console.log("Admin logged in"); return true;
} logout(): void {
console.log("Admin logged out");
}
}
An implementing class must provide all required members.
25. Utility Type: Partial<T>
Partial makes every property optional.
interface User {
name: string;
email: string;
age: number;
}function updateUser(
user: User,
updates: Partial<User>
): User {
return {
...user,
...updates
};
}const user: User = {
name: "Ravi",
email: "ravi@example.com",
age: 22
};const updatedUser = updateUser(user, {
age: 23
});console.log(updatedUser);
Useful for:
- PATCH requests
- Update forms
- Partial database updates
26. Utility Type: Required<T>
Required makes all properties mandatory.
interface UserSettings {
theme?: string;
notifications?: boolean;
}const settings: Required<UserSettings> = {
theme: "dark",
notifications: true
};27. Utility Type: Readonly<T>
Readonly prevents property updates.
interface Config {
port: number;
environment: string;
}const config: Readonly<Config> = {
port: 3000,
environment: "development"
};This produces an error:
config.port = 5000;28. Utility Type: Pick<T, K>
Pick creates a type using selected properties.
interface User {
id: number;
name: string;
email: string;
password: string;
}type PublicUser = Pick<
User,
"id" | "name" | "email"
>;const publicUser: PublicUser = {
id: 1,
name: "Ravi",
email: "ravi@example.com"
};
Useful for hiding sensitive fields.
29. Utility Type: Omit<T, K>
Omit removes selected properties.
interface User {
id: number;
name: string;
email: string;
password: string;
}type SafeUser = Omit<User, "password">;Example:
const safeUser: SafeUser = {
id: 1,
name: "Ravi",
email: "ravi@example.com"
};30. Utility Type: Record<K, T>
Record creates an object with defined keys and value types.
type UserRole = "admin" | "editor" | "user";type RolePermissions = Record<
UserRole,
string[]
>;const permissions: RolePermissions = {
admin: ["create", "read", "update", "delete"],
editor: ["create", "read", "update"],
user: ["read"]
};
31. Utility Type: Exclude<T, U>
Exclude removes members from a union.
type Status =
| "pending"
| "success"
| "failed"
| "cancelled";type ActiveStatus = Exclude<
Status,
"cancelled"
>;ActiveStatus becomes:
"pending" | "success" | "failed"32. Utility Type: ReturnType<T>
ReturnType extracts a function’s return type.
function createUser() {
return {
id: 1,
name: "Ravi",
active: true
};
}type CreatedUser = ReturnType<
typeof createUser
>;33. Handling Errors Safely
With strict TypeScript settings, caught errors should be treated carefully.
try {
throw new Error("Something went wrong");
} catch (error: unknown) {
if (error instanceof Error) {
console.log(error.message);
} else {
console.log("Unknown error");
}
}Avoid:
catch (error: any) {
console.log(error.message);
}34. Typed Async Functions
interface User {
id: number;
name: string;
}async function getUser(): Promise<User> {
return {
id: 1,
name: "Ravi"
};
}Typed array response:
async function getUsers(): Promise<User[]> {
return [
{
id: 1,
name: "Ravi"
}
];
}35. Generic API Response
interface ApiResponse<T> {
success: boolean;
message: string;
data: T;
}Function:
async function fetchData<T>(
url: string
): Promise<ApiResponse<T>> {
const response = await fetch(url);if (!response.ok) {
throw new Error(
`Request failed with ${response.status}`
);
} return response.json() as Promise<
ApiResponse<T>
>;
}
Usage:
interface User {
id: number;
name: string;
email: string;
}async function loadUsers(): Promise<void> {
try {
const response = await fetchData<User[]>(
"/api/users"
); console.log(response.data);
} catch (error: unknown) {
if (error instanceof Error) {
console.log(error.message);
}
}
}
Remember: TypeScript assertions do not validate API data at runtime. We will later use Zod for request and response validation.
36. Avoiding Unsafe any
Avoid:
function processData(data: any): any {
return data.value;
}Prefer:
interface Data {
value: string;
}function processData(data: Data): string {
return data.value;
}For uncertain data, prefer unknown:
function processUnknown(data: unknown): void {
if (typeof data === "string") {
console.log(data.toUpperCase());
}
}37. The never Type
never represents something that never returns normally.
function throwError(message: string): never {
throw new Error(message);
}It can also help ensure every possible case is handled.
type Status = "pending" | "success" | "failed";function assertNever(value: never): never {
throw new Error(
`Unhandled value: ${value}`
);
}function handleStatus(status: Status): string {
switch (status) {
case "pending":
return "Waiting"; case "success":
return "Completed"; case "failed":
return "Failed"; default:
return assertNever(status);
}
}
38. The satisfies Operator
satisfies checks that a value matches a type without losing its more specific inferred type.
type AppConfig = {
port: number;
environment:
| "development"
| "production";
};const config = {
port: 3000,
environment: "development"
} satisfies AppConfig;This is useful for configuration objects.
39. Mini Project — User Management System
type Role = "admin" | "editor" | "user";interface User {
readonly id: number;
name: string;
email: string;
role: Role;
active: boolean;
}type CreateUserInput = Omit<
User,
"id" | "active"
>;type UpdateUserInput = Partial<
Omit<User, "id">
>;class UserManager {
private users: User[] = [];
private nextId = 1; create(input: CreateUserInput): User {
const user: User = {
id: this.nextId++,
active: true,
...input
}; this.users.push(user); return user;
} getAll(): ReadonlyArray<User> {
return this.users;
} findById(id: number): User | undefined {
return this.users.find(
(user) => user.id === id
);
} update(
id: number,
updates: UpdateUserInput
): User {
const user = this.findById(id); if (!user) {
throw new Error("User not found");
} Object.assign(user, updates); return user;
} delete(id: number): boolean {
const index = this.users.findIndex(
(user) => user.id === id
); if (index === -1) {
return false;
} this.users.splice(index, 1); return true;
}
}
Using the system:
const manager = new UserManager();const user = manager.create({
name: "Ravi",
email: "ravi@example.com",
role: "admin"
});console.log(user);manager.update(user.id, {
name: "Ravi Kumar"
});console.log(manager.getAll());
This project combines:
- Interfaces
- Literal types
- Classes
- Private properties
- Generics through utility types
OmitPartialReadonlyArray- Error handling
- Type-safe CRUD operations
TypeScript Best Practices
Enable strict mode
{
"compilerOptions": {
"strict": true
}
}Prefer unknown over any
let data: unknown;Use inference for obvious local values
const port = 3000;You usually don’t need:
const port: number = 3000;Type function boundaries
function calculateTotal(
price: number,
quantity: number
): number {
return price * quantity;
}Use literal unions instead of unrestricted strings
Prefer:
type Role = "admin" | "user";Instead of:
let role: string;Keep types close to the related feature
Example:
users/
├── user.types.ts
├── user.service.ts
├── user.controller.ts
└── user.routes.tsDo not trust external data automatically
API data, user input and environment variables require runtime validation.
TypeScript checks code during development, but it does not automatically validate data while the application is running.
Common Beginner Mistakes
Using any to fix every error
const data: any = response;This removes TypeScript’s safety.
Overusing type assertions
const user = data as User;This does not prove that data is a valid user.
Creating overly complex types too early
Start with clear interfaces and simple unions. Add advanced types only when they improve reuse or safety.
Forgetting that TypeScript becomes JavaScript
Interfaces, type aliases and type annotations do not exist in the compiled JavaScript.
Important Concepts Learned Today
✅ Advanced unions
✅ Discriminated unions
✅ Intersection types
✅ Type guards
✅ Custom type guards
✅ Generics
✅ Generic constraints
✅ keyof
✅ Indexed access types
✅ Classes
✅ Access modifiers
✅ Inheritance
✅ Abstract classes
✅ Interface implementation
✅ Utility types
✅ Safe error handling
✅ Typed asynchronous code
✅ Generic API responses
✅ never
✅ satisfies
✅ TypeScript best practices
Homework
Create:
- Generic function that returns the last array item
- Generic API response interface
- Role permission system using
Record - Product update function using
Partial - Public user type using
Omit - Class-based bank account system
- Typed CRUD service for products
- Custom type guard for API data
Practice Challenge
Build a type-safe product manager with:
createProduct()
getProducts()
findProductById()
updateProduct()
deleteProduct()Use:
- Interface
- Class
- Private array
OmitPartialReadonlyArray- Error handling
Day 9
Node.js + TypeScript Setup
From Day 9 onward, the backend will use TypeScript only.
We’ll cover:
- Node.js architecture
- Project initialization
- TypeScript configuration for Node.js
tsxor development runner setup- npm scripts
- Node.js type definitions
- Environment variables
- ES modules
- Folder structure
- Git and
.gitignore - Development and production builds
You’ll create the foundation used by every upcoming backend project.





