From types to advanced patterns — 10 comprehensive lessons covering everything from primitives to mapped types, conditional types, and template literals.
TypeScript is a typed superset of JavaScript that compiles to plain JavaScript. It adds static type checking to help catch bugs at compile time rather than runtime. TypeScript files use the .ts extension and are compiled using the tsc compiler.
TypeScript supports all JavaScript primitive types plus some additional ones. You can explicitly annotate variables with types using : type syntax.
// String, number, boolean
let firstName: string = "Mayank";
let age: number = 25;
let isStudent: boolean = true;
// null and undefined
let nothing: null = null;
let notDefined: undefined = undefined;
// BigInt and Symbol
let big: bigint = 100n;
let sym: symbol = Symbol("id");
// Type inference — TypeScript figures out the type automatically
let city = "Nagpur"; // inferred as string
let score = 42; // inferred as number
// const vs let
const PI = 3.14159; // PI is narrowed to 3.14159 (literal type)
let radius = 10; // radius is number
// Type annotations on variables
let username: string = "mayank-dev-15";
let isLoggedIn: boolean = true;
Arrays can be typed using either the element type followed by [] or the Array<T> generic syntax. Tuples are fixed-length arrays with known types at each position.
// Array of numbers
let scores: number[] = [98, 87, 92, 100];
let names: Array<string> = ["Alice", "Bob", "Charlie"];
// Mixed types use union
let mixed: (string | number)[] = [1, "two", 3, "four"];
// Tuple — fixed length, fixed types per position
let person: [string, number] = ["Mayank", 25];
// person = [25, "Mayank"]; // Error: wrong order
// Readonly tuple
let coordinates: readonly [number, number] = [19.076, 72.8777];
// Destructuring with types
let [name, score] : [string, number] = ["Alice", 95];
Objects can be typed inline or using interfaces/types. TypeScript's type inference is powerful — it often knows the type without explicit annotation.
// Inline object type
let user: { name: string; age: number; email: string } = {
name: "Mayank",
age: 25,
email: "mayank@example.com"
};
// Type inference with objects
const car = {
make: "Toyota",
model: "Camry",
year: 2024
};
// TypeScript infers { make: string; model: string; year: number }
// Optional properties with ?
let config: { host: string; port?: number } = {
host: "localhost"
// port is optional
};
// readonly properties
let product: { readonly id: number; name: string } = {
id: 1,
name: "TypeScript Book"
};
product.name = "New Book"; // OK
// product.id = 2; // Error: readonly
let for variables, const for constants?) and readonly modifiers add safetylet and const in TypeScript?Interfaces define the shape of objects. They are one of the most powerful features of TypeScript and are used extensively in real-world codebases.
// Basic interface
interface User {
id: number;
name: string;
email: string;
age: number;
}
// Using the interface
const developer: User = {
id: 1,
name: "Mayank",
email: "mayank@example.com",
age: 25
};
// Optional properties with ?
interface Product {
id: number;
name: string;
description?: string; // optional
price: number;
inStock: boolean;
}
// Readonly properties
interface Config {
readonly apiUrl: string;
readonly timeout: number;
debug?: boolean;
}
const appConfig: Config = {
apiUrl: "https://api.example.com",
timeout: 5000
};
// appConfig.apiUrl = "other"; // Error: readonly
Type aliases use the type keyword and can represent unions, intersections, and more complex types that interfaces cannot express.
// Simple type alias
type ID = string | number;
type Status = "active" | "inactive" | "pending";
// Object type alias
type User = {
id: ID;
name: string;
email: string;
status: Status;
};
// Union type alias
type Result = Success | Failure;
type Success = { ok: true; data: any };
type Failure = { ok: false; error: string };
// Function type alias
type Callback = (data: string) => void;
type AsyncFn<T> = () => Promise<T>;
// Intersection type
type Employee = User & {
department: string;
salary: number;
};
// Mapped type alias
type Nullable<T> = { [K in keyof T]: T[K] | null };
Interfaces can be extended and merged (declaration merging), while types support intersections. Use interfaces for object shapes and types for unions/intersections.
// Interface extension
interface Animal {
name: string;
age: number;
}
interface Dog extends Animal {
breed: string;
bark(): void;
}
const myDog: Dog = {
name: "Rex",
age: 3,
breed: "Labrador",
bark() { console.log("Woof!"); }
};
// Multiple extension
interface Cat extends Animal {
indoor: boolean;
}
interface Pet extends Dog, Cat {
owner: string;
}
// Declaration merging — add to existing interface
interface Window {
myCustomProp: string;
}
// Type equivalent with intersection
type TypeDog = Animal & {
breed: string;
bark(): void;
};
// Interface can be declared multiple times (merged)
interface Cache {
get(key: string): any;
}
interface Cache {
set(key: string, value: any): void;
}
// Cache now has both get and set
interface for object shapes and class contracts. Use type for unions, intersections, tuples, and computed types.
Functions in TypeScript can have types on parameters and return values. This catches bugs at compile time and serves as excellent documentation.
// Explicit parameter and return types
function add(a: number, b: number): number {
return a + b;
}
// Return type inference (optional — TS infers it)
function multiply(a: number, b: number) {
return a * b; // inferred as number
}
// Arrow function with types
const divide = (a: number, b: number): number => {
if (b === 0) throw new Error("Division by zero");
return a / b;
};
// Void return type
function logMessage(msg: string): void {
console.log(msg);
}
// Never return type — function never returns
function throwError(msg: string): never {
throw new Error(msg);
}
// Optional parameters (must come last)
function greet(name: string, greeting?: string): string {
return `${greeting ?? "Hello"}, ${name}!`;
}
greet("Mayank"); // "Hello, Mayank!"
greet("Mayank", "Good morning"); // "Good morning, Mayank!"
// Default parameters
function createUser(name: string, role: string = "user"): object {
return { name, role };
}
Rest parameters allow functions to accept any number of arguments as an array. They work alongside typed parameters.
// Rest parameters
function sum(...numbers: number[]): number {
return numbers.reduce((total, n) => total + n, 0);
}
sum(1, 2, 3); // 6
sum(10, 20, 30, 40); // 100
// Rest with other parameters
function log(level: string, ...messages: string[]): void {
console.log(`[${level}]`, ...messages);
}
log("INFO", "Server started", "on port 3000");
// Object rest parameters
function mergeOptions(
defaults: { host: string; port: number },
overrides: Partial<{ host: string; port: number }>
) {
return { ...defaults, ...overrides };
}
// Destructured parameters with types
function displayUser({ name, age, email }: {
name: string;
age: number;
email: string;
}): string {
return `${name} (${age}) - ${email}`;
}
Function overloads let you define multiple call signatures for the same function. The implementation must handle all overloaded signatures.
// Overload signatures
function format(value: string): string;
function format(value: number): string;
function format(value: Date): string;
function format(value: string | number | Date): string {
if (typeof value === "string") {
return value.toUpperCase();
} else if (typeof value === "number") {
return value.toFixed(2);
} else {
return value.toISOString();
}
}
format("hello"); // "HELLO"
format(3.14159); // "3.14"
format(new Date()); // "2025-01-15T..."
// Real-world overload — API response parser
function parseResponse(data: string): object;
function parseResponse(data: ArrayBuffer): object;
function parseResponse(data: string | ArrayBuffer): object {
if (typeof data === "string") {
return JSON.parse(data);
} else {
const decoder = new TextDecoder();
return JSON.parse(decoder.decode(data));
}
}
// Generic overload for type-safe creation
function createArray<T>(length: number, fill: T): T[] {
return Array<T>({ length }).fill(fill);
}
createArray<number>(5, 0); // [0, 0, 0, 0, 0]
createArray<string>(3, ""); // ["", "", ""]
// Callback typed function
function fetchData<T>(
url: string,
callback: (error: Error | null, data?: T) => void
): void {
fetch(url)
.then(res => res.json())
.then(data => callback(null, data as T))
.catch(err => callback(err));
}
Generics allow you to write reusable code that works with any type while preserving type safety. They act as type parameters — like function arguments but for types.
// Basic generic function
function identity<T>(value: T): T {
return value;
}
identity<string>("hello"); // "hello" — T is string
identity<number>(42); // 42 — T is number
identity(true); // true — T inferred as boolean
// Multiple type parameters
function pair<A, B>(first: A, second: B): [A, B] {
return [first, second];
}
pair("Mayank", 25); // [string, number]
pair(1, true); // [number, boolean]
// Generic with arrays
function firstElement<T>(arr: T[]): T | undefined {
return arr[0];
}
firstElement([1, 2, 3]); // number
firstElement(["a", "b", "c"]); // string
firstElement([]); // undefined
// Generic arrow function
const wrap = <T>(value: T): { value: T } => ({ value });
wrap("hello"); // { value: "hello" }
wrap(42); // { value: 42 }
Interfaces and classes can also be generic, allowing you to define reusable shapes that work with any type.
// Generic interface
interface ApiResponse<T> {
data: T;
status: number;
message: string;
timestamp: Date;
}
// Using generic interface
interface User {
id: number;
name: string;
email: string;
}
const userResponse: ApiResponse<User> = {
data: { id: 1, name: "Mayank", email: "mayank@example.com" },
status: 200,
message: "Success",
timestamp: new Date()
};
// Generic class
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];
}
get size(): number {
return this.items.length;
}
isEmpty(): boolean {
return this.items.length === 0;
}
}
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
numberStack.pop(); // 2
const stringStack = new Stack<string>();
stringStack.push("hello");
// Generic interface for data structures
interface Repository<T> {
findById(id: string): Promise<T | null>;
findAll(): Promise<T[]>;
create(item: Omit<T, "id">): Promise<T>;
update(id: string, item: Partial<T>): Promise<T>;
delete(id: string): Promise<boolean>;
}
// Generic map/dictionary
interface Dictionary<T> {
[key: string]: T;
}
const scores: Dictionary<number> = {
math: 95,
science: 88,
english: 92
};
Constraints use extends to limit what types a generic can accept. Default types provide a fallback when no type is specified.
// Constraint — T must have a "length" property
function logLength<T extends { length: number }>(item: T): T {
console.log(`Length: ${item.length}`);
return item;
}
logLength("hello"); // OK — string has length
logLength([1, 2, 3]); // OK — array has length
// logLength(42); // Error: number has no "length"
// Constraint with keyof
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "Mayank", age: 25, email: "mayank@test.com" };
getProperty(user, "name"); // "Mayank" (string)
getProperty(user, "age"); // 25 (number)
// getProperty(user, "phone"); // Error: "phone" not in keyof User
// Default type parameters
interface PaginatedResponse<T = any> {
data: T[];
total: number;
page: number;
perPage: number;
}
// Uses default `any` when no type specified
const genericResponse: PaginatedResponse = {
data: [1, "hello", true],
total: 100,
page: 1,
perPage: 10
};
// Explicit type
const userResponse2: PaginatedResponse<User> = {
data: [{ id: 1, name: "Mayank", email: "mayank@test.com" }],
total: 50,
page: 1,
perPage: 10
};
// Multiple defaults
type Merge<A = {}, B = {}> = { [K in keyof A | keyof B]: K extends keyof A ? A[K] : K extends keyof B ? B[K] : never };
// Practical example — typed event emitter
class TypedEventEmitter<Events extends Record<string, any[]>> {
private listeners: { [K in keyof Events]?: ((...args: Events[K]) => void)[] } = {};
on<K extends keyof Events>(event: K, listener: (...args: Events[K]) => void): void {
if (!this.listeners[event]) this.listeners[event] = [];
this.listeners[event]!.push(listener);
}
emit<K extends keyof Events>(event: K, ...args: Events[K]): void {
this.listeners[event]?.forEach(fn => fn(...args));
}
}
// Usage
interface AppEvents {
login: [username: string, timestamp: Date];
logout: [];
purchase: [itemId: string, amount: number];
}
const emitter = new TypedEventEmitter<AppEvents>();
emitter.on("login", (user, time) => console.log(`${user} logged in at ${time}`));
emitter.on("purchase", (id, amt) => console.log(`Bought ${id} for $${amt}`));
// emitter.emit("purchase", 123, "wrong"); // Error: wrong types
anyUsing any defeats the purpose of generics. Use unknown when the type is truly unknown, and constrain generics with extends.A union type A | B means a value can be either type A or type B. TypeScript tracks which type is currently in scope through type narrowing.
// Simple union
type StringOrNumber = string | number;
let value: StringOrNumber = "hello";
value = 42; // OK
// value = true; // Error: boolean not in union
// Union with literals
type Direction = "up" | "down" | "left" | "right";
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type Theme = "light" | "dark" | "system";
function move(direction: Direction): void {
console.log(`Moving ${direction}`);
}
move("up"); // OK
// move("diagonal"); // Error
// Union of object types
type Success = { status: "success"; data: any };
type Error_ = { status: "error"; message: string };
type Loading = { status: "loading" };
type RequestState = Success | Error_ | Loading;
function handleState(state: RequestState) {
switch (state.status) {
case "success":
console.log("Data:", state.data); // TS knows this is Success
break;
case "error":
console.log("Error:", state.message); // TS knows this is Error_
break;
case "loading":
console.log("Loading..."); // TS knows this is Loading
break;
}
}
Discriminated unions use a common literal property (the "discriminant") to distinguish between union members. This is one of TypeScript's most powerful patterns.
// Discriminated union with "kind" discriminant
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rectangle"; width: number; height: number }
| { kind: "triangle"; base: number; height: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "rectangle":
return shape.width * shape.height;
case "triangle":
return (shape.base * shape.height) / 2;
}
}
const circle: Shape = { kind: "circle", radius: 5 };
const rect: Shape = { kind: "rectangle", width: 4, height: 6 };
console.log(area(circle)); // 78.539...
console.log(area(rect)); // 24
// Exhaustive check pattern
function areaExhaustive(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "rectangle":
return shape.width * shape.height;
case "triangle":
return (shape.base * shape.height) / 2;
default:
const _exhaustive: never = shape; // Compile error if case missing
return _exhaustive;
}
}
// Real-world API response union
type ApiResponse =
| { type: "user"; id: string; name: string; email: string }
| { type: "product"; id: string; title: string; price: number }
| { type: "error"; code: number; message: string };
function processResponse(res: ApiResponse): string {
switch (res.type) {
case "user":
return `User: ${res.name} (${res.email})`;
case "product":
return `Product: ${res.title} — $${res.price}`;
case "error":
return `Error ${res.code}: ${res.message}`;
}
}
TypeScript narrows types through control flow analysis and type guard functions. Common guards include typeof, instanceof, in, and custom predicates.
// typeof guard
function process(value: string | number | boolean) {
if (typeof value === "string") {
return value.toUpperCase(); // narrowed to string
} else if (typeof value === "number") {
return value.toFixed(2); // narrowed to number
} else {
return value ? "yes" : "no"; // narrowed to boolean
}
}
// instanceof guard
function formatDate(input: string | Date): string {
if (input instanceof Date) {
return input.toLocaleDateString(); // narrowed to Date
}
return new Date(input).toLocaleDateString(); // narrowed to string
}
// in operator guard
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move2(animal: Fish | Bird) {
if ("swim" in animal) {
animal.swim(); // narrowed to Fish
} else {
animal.fly(); // narrowed to Bird
}
}
// Custom type guard (type predicate)
function isFish(animal: Fish | Bird): animal is Fish {
return "swim" in animal;
}
function move3(animal: Fish | Bird) {
if (isFish(animal)) {
animal.swim();
} else {
animal.fly();
}
}
// Assertion function
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== "string") {
throw new Error(`Expected string, got ${typeof value}`);
}
}
function processInput(input: unknown) {
assertIsString(input); // narrows to string after this line
console.log(input.toUpperCase());
}
// Intersection types — combine multiple types
type HasName = { name: string };
type HasAge = { age: number };
type HasEmail = { email: string };
type Person = HasName & HasAge & HasEmail;
const person: Person = {
name: "Mayank",
age: 25,
email: "mayank@test.com"
};
// Intersection with interfaces
interface Serializable {
serialize(): string;
}
interface Loggable {
log(): void;
}
// Class must implement both
class MyClass implements Serializable, Loggable {
serialize(): string { return "{}"; }
log(): void { console.log("logged"); }
}
// Function intersection (overloaded)
type Fn = ((x: number) => number) & ((x: string) => string);
as anyIt bypasses all type checking. Use type guards and narrowing instead to maintain type safety.TypeScript ships with built-in utility types that transform existing types. These are essential for writing flexible, reusable code.
interface User {
id: number;
name: string;
email: string;
age: number;
role: "admin" | "user";
}
// Partial — all properties become optional
type PartialUser = Partial<User>;
// Equivalent to:
// { id?: number; name?: string; email?: string; age?: number; role?: "admin" | "user" }
function updateUser(id: number, updates: Partial<User>) {
// Only update provided fields
console.log(`Updating user ${id}`, updates);
}
updateUser(1, { name: "New Name" }); // OK
updateUser(1, { email: "new@test.com", age: 30 }); // OK
// Required — all properties become required
interface Config {
host?: string;
port?: number;
debug?: boolean;
}
type RequiredConfig = Required<Config>;
// { host: string; port: number; debug: boolean }
// Pick — select specific properties
type UserBasic = Pick<User, "id" | "name" | "email">;
// { id: number; name: string; email: string }
function displayUser(user: Pick<User, "name" | "email">) {
return `${user.name} (${user.email})`;
}
// Omit — remove specific properties
type CreateUserInput = Omit<User, "id" | "role">;
// { name: string; email: string; age: number }
const newUser: CreateUserInput = {
name: "Mayank",
email: "mayank@test.com",
age: 25
};
Record creates object types with consistent value types. Readonly makes all properties immutable. These work alongside the others for complete type transformation.
// Record — key-value type builder
type Scores = Record<string, number>;
const testScores: Scores = {
math: 95,
science: 88,
english: 92
};
// Record with union keys
type StatusMessages = Record<"loading" | "success" | "error", string>;
const messages: StatusMessages = {
loading: "Please wait...",
success: "Done!",
error: "Something went wrong"
};
// Readonly — immutable object
interface GameState {
player: string;
score: number;
level: number;
}
const state: Readonly<GameState> = {
player: "Mayank",
score: 0,
level: 1
};
// state.score = 100; // Error: readonly
// state.level = 2; // Error: readonly
// ReadonlyArray and ReadonlyMap
const items: readonly string[] = ["a", "b", "c"];
// items.push("d"); // Error
const cache: ReadonlyMap<string, any> = new Map([["key", "value"]]);
// cache.set("new", 1); // Error
// Deep readonly
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? DeepReadonly<T[K]>
: T[K];
};
interface Nested {
user: {
name: string;
address: {
city: string;
zip: string;
};
};
}
type FrozenNested = DeepReadonly<Nested>;
// Practical combination — partial update type
type UpdatePayload<T> = Partial<Omit<T, "id" | "createdAt" | "updatedAt">>;
interface BlogPost {
id: string;
title: string;
content: string;
author: string;
createdAt: Date;
updatedAt: Date;
}
type BlogUpdate = UpdatePayload<BlogPost>;
// { title?: string; content?: string; author?: string }
ReturnType and Parameters extract types from function signatures, enabling powerful type-safe patterns.
// ReturnType — extract return type
function createUser(name: string, age: number) {
return { id: Math.random(), name, age, createdAt: new Date() };
}
type User2 = ReturnType<typeof createUser>;
// { id: number; name: string; age: number; createdAt: Date }
// Parameters — extract parameter types as tuple
type CreateUserParams = Parameters<typeof createUser>;
// [name: string, age: number]
function makeUser(...args: CreateUserParams) {
return createUser(...args);
}
makeUser("Mayank", 25); // OK
// makeUser("Mayank"); // Error: missing age
// ConstructorParameters — extract constructor params
class Logger {
constructor(private prefix: string, private verbose: boolean) {}
}
type LoggerParams = ConstructorParameters<typeof Logger>;
// [prefix: string, verbose: boolean]
// Awaiting — extract promise result type
async function fetchData(): Promise<{ users: User[] }> {
return { users: [] };
}
type FetchResult = Awaited<ReturnType<typeof fetchData>>;
// { users: User[] }
// InstanceType — extract instance type from class
class Container {
value: number = 42;
}
type ContainerInstance = InstanceType<typeof Container>;
// Container
// Practical — type-safe function registry
type FnMap = Record<string, (...args: any[]) => any>;
function createRegistry<T extends FnMap>(fns: T) {
return {
execute<K extends keyof T>(
name: K,
...args: Parameters<T[K]>
): ReturnType<T[K]> {
return fns[name](...args);
}
};
}
const mathFns = {
add: (a: number, b: number) => a + b,
multiply: (a: number, b: number) => a * b,
greet: (name: string) => `Hello, ${name}!`
};
const registry = createRegistry(mathFns);
registry.execute("add", 1, 2); // 3
registry.execute("greet", "Mayank"); // "Hello, Mayank!"
TypeScript classes support public, private, and protected access modifiers. readonly makes properties immutable after construction.
class User {
public id: number; // accessible everywhere
private password: string; // accessible only inside class
protected email: string; // accessible in class and subclasses
readonly createdAt: Date; // immutable after construction
constructor(
id: number,
name: string,
email: string,
password: string
) {
this.id = id;
this.email = email;
this.password = password;
this.createdAt = new Date();
}
// Public method
getProfile(): string {
return `User ${this.id}: ${this.email}`;
}
// Private method
private hashPassword(): string {
return this.password.split("").reverse().join("");
}
// Protected method — accessible in subclasses
protected validateEmail(): boolean {
return this.email.includes("@");
}
// Getter
get displayEmail(): string {
return this.email.replace(/(.{2}).*(@.*)/, "$1***$2");
}
// Setter
set newPassword(value: string) {
if (value.length < 8) {
throw new Error("Password must be at least 8 characters");
}
this.password = value;
}
}
const user = new User(1, "Mayank", "mayank@test.com", "secret123");
console.log(user.getProfile()); // OK
console.log(user.displayEmail); // "ma***@test.com"
// console.log(user.password); // Error: private
// console.log(user.email); // Error: protected
Abstract classes cannot be instantiated directly. They define contracts for subclasses and can include both abstract (unimplemented) and concrete methods.
// Abstract class
abstract class Shape {
abstract area(): number;
abstract perimeter(): number;
describe(): string {
return `Area: ${this.area().toFixed(2)}, Perimeter: ${this.perimeter().toFixed(2)}`;
}
}
class Circle extends Shape {
constructor(private radius: number) {
super();
}
area(): number {
return Math.PI * this.radius ** 2;
}
perimeter(): number {
return 2 * Math.PI * this.radius;
}
}
class Rectangle extends Shape {
constructor(
private width: number,
private height: number
) {
super();
}
area(): number {
return this.width * this.height;
}
perimeter(): number {
return 2 * (this.width + this.height);
}
}
// const s = new Shape(); // Error: cannot instantiate abstract class
const circle = new Circle(5);
const rect = new Rectangle(4, 6);
console.log(circle.describe()); // "Area: 78.54, Perimeter: 31.42"
console.log(rect.describe()); // "Area: 24.00, Perimeter: 20.00"
// Abstract class with abstract and concrete methods
abstract class Repository<T> {
protected items: T[] = [];
abstract findById(id: string): T | undefined;
abstract create(item: T): T;
findAll(): T[] {
return [...this.items];
}
count(): number {
return this.items.length;
}
}
interface Todo {
id: string;
title: string;
completed: boolean;
}
class TodoRepository extends Repository<Todo> {
findById(id: string): Todo | undefined {
return this.items.find(item => item.id === id);
}
create(item: Todo): Todo {
this.items.push(item);
return item;
}
}
Classes can implement one or more interfaces, enforcing that they provide specific methods and properties. This is TypeScript's way of supporting polymorphism.
// Interfaces to implement
interface Printable {
print(): string;
}
interface Serializable {
serialize(): string;
}
interface Comparable<T> {
compareTo(other: T): number;
}
// Implementing multiple interfaces
class Document implements Printable, Serializable, Comparable<Document> {
constructor(
public title: string,
public content: string,
public createdAt: Date
) {}
print(): string {
return `[${this.title}]\n${this.content}`;
}
serialize(): string {
return JSON.stringify({
title: this.title,
content: this.content,
createdAt: this.createdAt.toISOString()
});
}
compareTo(other: Document): number {
return this.createdAt.getTime() - other.createdAt.getTime();
}
}
// Interface for dependency injection
interface Logger {
log(message: string): void;
error(message: string): void;
}
interface Database {
query<T>(sql: string): Promise<T[]>;
execute(sql: string): Promise<void>;
}
class UserService {
constructor(
private logger: Logger,
private db: Database
) {}
async getUser(id: string) {
this.logger.log(`Fetching user ${id}`);
const users = await this.db.query<User>(
`SELECT * FROM users WHERE id = '${id}'`
);
return users[0];
}
}
// Mock implementation for testing
class ConsoleLogger implements Logger {
log(message: string): void { console.log(`[LOG] ${message}`); }
error(message: string): void { console.error(`[ERR] ${message}`); }
}
// Static members
class MathUtils {
static readonly PI = 3.14159;
static readonly E = 2.71828;
static clamp(value: number, min: number, max: number): number {
return Math.min(Math.max(value, min), max);
}
static lerp(a: number, b: number, t: number): number {
return a + (b - a) * t;
}
}
MathUtils.clamp(15, 0, 10); // 10
MathUtils.lerp(0, 100, 0.5); // 50
Enums define a set of named constants. Numeric enums auto-increment from 0 by default, or you can set explicit values.
// Basic numeric enum (auto-increments from 0)
enum Direction {
Up, // 0
Down, // 1
Left, // 2
Right // 3
}
let dir: Direction = Direction.Up;
console.log(dir); // 0
console.log(Direction[0]); // "Up" (reverse mapping)
// Explicit values
enum HttpStatus {
OK = 200,
Created = 201,
BadRequest = 400,
Unauthorized = 401,
NotFound = 404,
ServerError = 500
}
function handleStatus(status: HttpStatus) {
switch (status) {
case HttpStatus.OK:
console.log("Success");
break;
case HttpStatus.NotFound:
console.log("Not found");
break;
}
}
handleStatus(HttpStatus.NotFound); // "Not found"
// Enums with computed values
enum Color {
Red = "#ff0000",
Green = "#00ff00",
Blue = "#0000ff"
}
const bg: Color = Color.Red;
// Bit flag enums
enum Permission {
None = 0,
Read = 1, // 0001
Write = 2, // 0010
Execute = 4, // 0100
All = Read | Write | Execute // 0111
}
function hasPermission(user: Permission, flag: Permission): boolean {
return (user & flag) === flag;
}
const userPerm = Permission.Read | Permission.Write;
hasPermission(userPerm, Permission.Read); // true
hasPermission(userPerm, Permission.Execute); // false
String enums require explicit values and produce more readable output. Const enums are inlined at compile time for better performance.
// String enum — no auto-increment, must set values
enum Status {
Active = "ACTIVE",
Inactive = "INACTIVE",
Pending = "PENDING",
Banned = "BANNED"
}
const userStatus: Status = Status.Active;
console.log(userStatus); // "ACTIVE" (not a number)
// String enum for API constants
enum ApiEndpoint {
Users = "/api/users",
Products = "/api/products",
Orders = "/api/orders",
Auth = "/api/auth"
}
function getUrl(endpoint: ApiEndpoint): string {
return `https://api.example.com${endpoint}`;
}
getUrl(ApiEndpoint.Users); // "https://api.example.com/api/users"
// Const enum — inlined at compile time, no runtime object
const enum LogLevel {
Debug = "DEBUG",
Info = "INFO",
Warn = "WARN",
Error = "ERROR"
}
// After compilation, this becomes just "ERROR"
const level = LogLevel.Error;
// Const enum with numeric values
const enum Season {
Spring, // 0
Summer, // 1
Autumn, // 2
Winter // 3
}
const currentSeason = Season.Summer; // compiles to: const currentSeason = 1;
// Enum as type guard
function isStatus(value: unknown): value is Status {
return Object.values(Status).includes(value as Status);
}
function processStatus(input: string) {
if (isStatus(input)) {
console.log(`Valid status: ${input}`);
}
}
Tuples are fixed-length arrays where each position has a known type. They are useful for function returns, key-value pairs, and structured data.
// Basic tuple
let user2: [string, number] = ["Mayank", 25];
// user2 = [25, "Mayank"]; // Error: wrong types
// Accessing tuple elements (type-aware)
const name2: string = user2[0]; // TypeScript knows index 0 is string
const age2: number = user2[1]; // TypeScript knows index 1 is number
// Named tuple elements (documentation only)
type UserTuple = [name: string, age: number, email: string];
const mayank: UserTuple = ["Mayank", 25, "mayank@test.com"];
// Tuple with optional elements
type Point = [number, number, number?];
const point2D: Point = [10, 20];
const point3D: Point = [10, 20, 30];
// Readonly tuple
type ReadonlyPoint = readonly [number, number];
const origin: ReadonlyPoint = [0, 0];
// origin[0] = 5; // Error: readonly
// Tuple as function return type
function useState<T>(initial: T): [T, (value: T) => void] {
let state = initial;
const setState = (value: T) => { state = value; };
return [state, setState];
}
const [count, setCount] = useState(0);
setCount(5);
// Tuple with rest elements
type StringArray = [string, ...string[], number];
const arr1: StringArray = ["a", 1];
const arr2: StringArray = ["a", "b", "c", 1];
// Destructured tuples with types
function splitName(fullName: string): [string, string] {
const parts = fullName.split(" ");
return [parts[0], parts[1]];
}
const [first, last] = splitName("Mayank Basena");
// Labeling tuple parameters
type Range = [start: number, end: number];
type KeyValue = [key: string, value: any];
function createRange(start: number, end: number): Range {
return [start, end];
}
function createEntry(key: string, value: any): KeyValue {
return [key, value];
}
// Tuple with readonly spread
type NamedNumber = readonly [string, ...number[]];
const data: NamedNumber = ["scores", 95, 88, 92];
// Real-world — useState pattern
function useToggle(initial: boolean): [boolean, () => void, (value: boolean) => void] {
let value = initial;
const toggle = () => { value = !value; };
const set = (v: boolean) => { value = v; };
return [value, toggle, set];
}
const [isOpen, toggleOpen, setIsOpen] = useToggle(false);
TypeScript uses ES module syntax (import/export) for code organization. Each .ts file is a module by default.
// ===== math.ts =====
// Named exports
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.14159;
// Export interface and type
export interface Point {
x: number;
y: number;
}
export type Direction2 = "up" | "down" | "left" | "right";
// ===== app.ts =====
// Named imports
import { add, subtract, PI, type Point, type Direction2 } from "./math";
// Import with alias
import { add as sum } from "./math";
// Default export (one per file)
// math.ts
export default class Calculator {
add(a: number, b: number) { return a + b; }
subtract(a: number, b: number) { return a - b; }
}
// app.ts
import Calculator from "./math";
const calc = new Calculator();
// Import everything as namespace
import * as MathUtils from "./math";
MathUtils.add(1, 2);
MathUtils.PI;
// Re-exporting (barrel files)
// index.ts — re-export all
export { add, subtract, PI } from "./math";
export type { Point, Direction2 } from "./math";
// Conditional re-export
export { add, subtract } from "./math";
Type-only imports/exports are erased at compile time and don't produce JavaScript. They prevent circular dependencies and clarify intent.
// Type-only imports — removed at compile time
import type { User, Config } from "./types";
import { type Status, createUser } from "./api";
// Without `type`, both Status and createUser are imported
// With `type`, only Status is imported as type (no runtime import)
// Type-only exports
export type { User, Config } from "./types";
// Inline type annotation
function processUser(user: import("./types").User) {
console.log(user.name);
}
// Declaration file (.d.ts) — ambient types
// globals.d.ts
declare global {
interface Window {
appVersion: string;
}
const API_URL: string;
}
// Module declaration for non-TS files
declare module "*.css" {
const content: Record<string, string>;
export default content;
}
declare module "*.svg" {
const content: string;
export default content;
}
declare module "legacy-module" {
export function doSomething(): void;
export interface LegacyConfig {
host: string;
port: number;
}
}
// Namespace declarations
namespace Validation {
export interface Validator {
validate(value: string): boolean;
}
export class EmailValidator implements Validator {
validate(value: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
}
}
export class PhoneValidator implements Validator {
validate(value: string): boolean {
return /^\d{10}$/.test(value);
}
}
}
const emailValidator = new Validation.EmailValidator();
emailValidator.validate("test@example.com"); // true
Namespaces group related code and avoid global pollution. Modern codebases prefer ES modules, but namespaces are useful for type organization.
// Namespace pattern (less common now)
namespace AppConfig {
export interface Config {
apiUrl: string;
timeout: number;
}
export const defaults: Config = {
apiUrl: "https://api.example.com",
timeout: 5000
};
export function merge(
base: Config,
overrides: Partial<Config>
): Config {
return { ...base, ...overrides };
}
}
// Usage
const config: AppConfig.Config = AppConfig.merge(AppConfig.defaults, {
timeout: 10000
});
// Namespace merging
namespace Animals {
export class Dog {
bark() { return "Woof!"; }
}
}
namespace Animals {
export class Cat {
meow() { return "Meow!"; }
}
}
// Both Dog and Cat are available
const dog = new Animals.Dog();
const cat = new Animals.Cat();
// Ambient module declarations
// types.d.ts
declare module "express" {
interface Request {
body: any;
params: Record<string, string>;
}
interface Response {
json(data: any): void;
status(code: number): Response;
}
function express(): {
use(middleware: (req: Request, res: Response, next: () => void) => void): void;
get(path: string, handler: (req: Request, res: Response) => void): void;
listen(port: number): void;
};
export default express;
export type { Request, Response };
}
// Package.json type resolution
// tsconfig.json should have:
// "compilerOptions": {
// "moduleResolution": "node",
// "esModuleInterop": true,
// "baseUrl": "./src",
// "paths": {
// "@/*": ["./*"],
// "@components/*": ["components/*"],
// "@utils/*": ["utils/*"]
// }
// }
import/export) for new code. Namespaces are mainly for declaration merging and ambient types.Mapped types iterate over keys of an existing type to create new types. They are the foundation of many utility types and enable powerful type transformations.
// Basic mapped type
type Stringify<T> = {
[K in keyof T]: string;
};
interface User3 {
id: number;
name: string;
email: string;
}
type StringUser = Stringify<User3>;
// { id: string; name: string; email: string }
// Mapping with modifiers
type OptionalAll<T> = {
[K in keyof T]?: T[K];
};
type ReadonlyAll<T> = {
+readonly [K in keyof T]: T[K]; // + adds modifier
};
type Mutable<T> = {
-readonly [K in keyof T]: T[K]; // - removes modifier
};
// Practical — form field types
type FormFields<T> = {
[K in keyof T]: {
value: T[K];
error?: string;
touched: boolean;
label: string;
};
};
interface UserData {
name: string;
email: string;
age: number;
}
type UserForm = FormFields<UserData>;
// {
// name: { value: string; error?: string; touched: boolean; label: string };
// email: { value: string; error?: string; touched: boolean; label: string };
// age: { value: number; error?: string; touched: boolean; label: string };
// }
// Mapped type with key remapping (TS 4.1+)
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = Getters<User3>;
// { getId: () => number; getName: () => string; getEmail: () => string }
// Filter mapped type by value type
type PickByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
};
interface Mixed {
name: string;
age: number;
email: string;
count: number;
}
type NumberProps = PickByType<Mixed, number>;
// { age: number; count: number }
Conditional types follow the pattern T extends U ? X : Y. They enable type-level logic and are essential for advanced generic patterns.
// Basic conditional type
type IsString<T> = T extends string ? true : false;
type A = IsString<string>; // true
type B = IsString<number>; // false
// Distributive conditional types
type ToArray<T> = T extends any ? T[] : never;
type StrArr = ToArray<string | number>;
// string[] | number[] (distributes over union)
// Non-distributive with brackets
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type Both = ToArrayNonDist<string | number>;
// (string | number)[]
// infer keyword — extract types from structures
type ElementType<T> = T extends (infer E)[] ? E : T;
type Nums = ElementType<number[]>; // number
type Str = ElementType<string>; // string (not array, returns T)
// Extract function return type manually
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type R1 = MyReturnType<() => string>; // string
type R2 = MyReturnType<(x: number) => void>; // void
// Extract function parameters manually
type MyParameters<T> = T extends (...args: infer P) => any ? P : never;
type P1 = MyParameters<(a: string, b: number) => void>; // [a: string, b: number]
// Extract promise inner type
type Awaited2<T> = T extends Promise<infer U> ? Awaited2<U> : T;
type Inner = Awaited2<Promise<Promise<string>>>; // string
// DeepPartial — recursive conditional
type DeepPartial2<T> = T extends object
? { [K in keyof T]?: DeepPartial2<T[K]> }
: T;
// Practical — API response type builder
type ApiSuccess<T> = { ok: true; data: T; status: number };
type ApiError = { ok: false; error: string; code: number };
type ApiResult<T> = ApiSuccess<T> | ApiError;
type ExtractData<T> = T extends ApiSuccess<infer D> ? D : never;
type UserData2 = ExtractData<ApiResult<{ id: number; name: string }>>;
// { id: number; name: string }
Template literal types create string union types from patterns. Combined with mapped types, they enable type-safe string manipulation at the type level.
// Basic template literal type
type Greeting = `Hello, ${string}!`;
const g1: Greeting = "Hello, World!"; // OK
// const g2: Greeting = "Hi, World!"; // Error
// Union in template literal
type Color3 = "red" | "blue" | "green";
type Size = "sm" | "md" | "lg";
type ColorSize = `${Color3}-${Size}`;
// "red-sm" | "red-md" | "red-lg" | "blue-sm" | "blue-md" | ...
// Practical — CSS units
type CSSValue = `${number}${"px" | "rem" | "em" | "%" | "vh" | "vw"}`;
const width: CSSValue = "100px"; // OK
const height: CSSValue = "50vh"; // OK
// const bad: CSSValue = "100"; // Error: missing unit
// Intrinsic string methods as types
type Upper = Uppercase<"hello">; // "HELLO"
type Lower = Lowercase<"HELLO">; // "hello"
type Cap = Capitalize<"hello">; // "Hello"
type Unc = Uncapitalize<"Hello">; // "hello"
// Event name patterns
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<"click">; // "onClick"
type FocusEvent = EventName<"focus">; // "onFocus"
// API route patterns
type ApiRoute = `/api/${"users" | "posts" | "comments"}`;
type UserRoute = `${ApiRoute}/${number}`;
// "/api/users/123" | "/api/posts/456" | "/api/comments/789"
// Deep path accessor
type NestedObj = {
user: {
profile: {
name: string;
settings: {
theme: string;
language: string;
};
};
};
};
type DotPrefix<T extends string> = T extends "" ? "" : `.${T}`;
type DotPaths<T> = {
[K in keyof T & string]: T[K] extends object
? `${K}${DotPrefix<DotPaths<T[K]>>}`
: K
}[keyof T & string];
type AllPaths = DotPaths<NestedObj>;
// "user" | "user.profile" | "user.profile.name" | "user.profile.settings" | ...
// Type-safe event system
type EventMap = {
click: { x: number; y: number };
keydown: { key: string; code: string };
resize: { width: number; height: number };
};
type EventKey = keyof EventMap; // "click" | "keydown" | "resize"
function on<K extends EventKey>(
event: K,
handler: (payload: EventMap[K]) => void
): void {
// implementation
}
on("click", ({ x, y }) => console.log(x, y)); // OK
on("keydown", ({ key }) => console.log(key)); // OK
// on("scroll", () => {}); // Error: "scroll" not in EventMap