Skip to content

Rayied991/TypeScript-Domination

Repository files navigation

πŸš€ Complete TypeScript Guide

πŸ“Œ What is TypeScript?

TypeScript is a strongly typed superset of JavaScript that compiles down to plain JavaScript. Think of it like this:

  • JavaScript = a kid without discipline
  • TypeScript = a kid with discipline

It provides type safety, catches errors at compile time, and makes large-scale applications more maintainable.


βœ… Why Do We Need TypeScript?

Benefits

  • Static Typing β†’ Errors are caught at compile time, not runtime
  • Code Completion & IntelliSense β†’ Better developer experience
  • Refactoring Support β†’ Safer code changes
  • Shorthand Notations β†’ Cleaner and more maintainable code

πŸ‘‰ In short: TS = JS + Type Checking

Drawbacks

  • Requires compilation (.ts β†’ transpiled β†’ .js)
  • Enforces disciplined coding (stricter syntax)

βš™οΈ Installation & Setup

# Install TypeScript globally
npm install -g typescript

Initialize TS Config (for ES6+)

tsc --init

This generates a tsconfig.json file for compiler settings.

Compile TypeScript

tsc app.ts              # compile manually
tsc --watch             # auto-compile on file change

πŸ“ TypeScript Type System

JavaScript Built-in Types

  • number
  • string
  • boolean
  • null
  • undefined
  • object

TypeScript Adds Extra Types

  • any
  • unknown
  • never
  • enum
  • tuple

🏷️ Basic Types & Primitives

Primitive Types

let id: number = 101;
let username: string = "Alice";
let isActive: boolean = true;

Arrays

let nums: number[] = [1, 2, 3, 4];
let mixed: (number | string)[] = [1, "two", 3]; // union type array

Tuples (Fixed-length Arrays with Defined Types)

let user: [number, string] = [1, "Alice"];
let coordinate: [number, string] = [12, "six"];

πŸ“ Tuples define exactly what type goes at each position in an array with fixed length.

Enums (Named Constants)

enum UserRoles {
  ADMIN = 12,
  GUEST = "guest",
  SUPER_ADMIN = "superadmin",
}

enum StatusCodes {
  ABANDONED = "500",
  NOT_FOUND = "404",
}

console.log(UserRoles.ADMIN);        // 12
console.log(StatusCodes.NOT_FOUND);  // "404"

βœ” Use for: status codes, user roles, configuration values
βœ” Benefit: More readable than magic numbers/strings


🎯 Special Types Explained

any - Turn Off TypeScript

let data: any = 123;
data = "hello";
data = true;
// data.toUpperCase(); // No error shown, but might fail at runtime

⚠️ Avoid using any - it defeats the purpose of TypeScript!

unknown - Type-safe Alternative to any

let input: unknown;
input = 12;
input = "ray";

// Type narrowing required
if (typeof input === "string") {
  input.toUpperCase(); // βœ… Safe
}

βœ… Use unknown instead of any - forces you to check the type

void - No Return Value

function logMessage(): void {
  console.log("No return value");
}

function getStatus(): boolean {
  return true; // must return boolean
}

null and undefined

let cafe: string | null = null;
cafe = "Starbucks"; // βœ…
// cafe = 12; // ❌ Error

let uninitialized: undefined;

never - Function Never Returns

function throwError(): never {
  throw new Error("Something went wrong");
}

function infiniteLoop(): never {
  while (true) {
    // runs forever
  }
}

// Code after never-returning function is unreachable
throwError();
console.log("This will never run"); // ⚠️ Unreachable

πŸ”„ Primitive vs Reference Types

Primitives (Copied by VALUE)

let a = 12;
let b = a;
b = 20;
console.log(a); // 12 (unchanged)
console.log(b); // 20

Reference Types (Copied by REFERENCE)

let arrA = [1, 2, 3, 4, 5];
let arrB = arrA; // points to same array
arrB.pop();      // modifies both
console.log(arrA); // [1, 2, 3, 4]
console.log(arrB); // [1, 2, 3, 4]

βœ” Primitives: number, string, boolean β†’ copied directly
βœ” References: arrays, objects β†’ share memory address


🎭 Type Inference vs Type Annotations

Type Inference

TypeScript automatically detects the type based on the assigned value.

let age = 25;           // TS infers: number
let username = "Rayied"; // TS infers: string
let isActive = true;    // TS infers: boolean

βœ… Less verbose
βœ… Cleaner code
⚠️ TypeScript still enforces the inferred type

Type Annotations

You explicitly specify the type. Useful when the type isn't obvious or when defining function parameters.

let age: number = 25;
let username: string = "Rayied";
let isActive: boolean = true;

// Multiple types allowed (Union)
let value: number | boolean | string;
value = 12;    // βœ…
value = "13";  // βœ…
value = false; // βœ…

βœ… More control
βœ… Better documentation
βœ… Prevents accidental type changes

Function Parameters & Return Types

function greet(name: string, age: number): string {
  return `Hello ${name}, you are ${age} years old`;
}

function calculate(a: number, b: string): void {
  console.log(a, b); // no return
}

πŸ”§ Functions in Depth

Function Types

Function types define the signature of a function - its parameter types and return type.

// Function with no return value
function abcd(): void {
  console.log("No return");
}

// Function that returns a string
function getMessage(): string {
  return "Hey";
}

Callback Functions with Types

function abc(name: string, cb: (value: string) => void) {
  cb(name);
}

abc("harsh", (value) => console.log(value));

function abcdef(name: string, age: number, cb: (arg: string) => void) {
  cb("hey");
}

abcdef("ray", 25, (arg: string) => console.log("abcdef", arg));

Callback Type Breakdown:

  • (value: string) => void means:
    • Function takes one parameter of type string
    • Returns nothing (void)

Optional Parameters

Optional parameters allow you to make some parameters not required. Use ? after the parameter name.

function abca(name: string, age: number, gender?: string) {
  console.log(name, age, gender);
}

abca("ray", 25, "male");  // βœ… all parameters
abca("alu", 22);          // βœ… gender is optional

⚠️ Optional parameters must come after required parameters

Default Parameters

Default parameters provide a fallback value if no argument is passed.

function abcab(name: string, age: number, gender: string = "not to be disclosed") {
  console.log(name, age, gender);
}

abcab("ray", 25);        // Uses default: "not to be disclosed"
abcab("alu", 22, "male"); // Uses provided: "male"

Default vs Optional:

  • Optional (?): Parameter can be undefined
  • Default (=): Parameter has a fallback value

Rest Parameters

Rest parameters allow a function to accept any number of arguments as an array.

// Rest parameters with ... (spread operator)
function sum(...arr: number[]) {
  console.log(arr); // [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
}

sum(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

function friends(...args: string[]) {
  console.log(args);    // ["ray", "angi", "alif"]
  console.log(args[0]); // "ray"
}

friends("ray", "angi", "alif");

How Rest Parameters Work:

  • ...arr collects all arguments into an array
  • Must be the last parameter in function signature
  • Type is always an array type

Spread Operator Example:

let arr = [12, 34, 5, 6];
let arr2 = [...arr]; // Creates a copy

Function Overloading

Function overloading allows you to define multiple function signatures for the same function name.

// Overload signatures
function overload(a: string): void;
function overload(a: string, b: number): number;

// Implementation signature
function overload(a: any, b?: any) {
  if (typeof a === "string" && b === undefined) {
    console.log("hey");
  }
  if (typeof a === "string" && typeof b === "number") {
    return 123;
  } else {
    throw new Error("something is wrong");
  }
}

overload("hel");      // βœ… Matches first signature
overload("hey", 12);  // βœ… Matches second signature

Function Overloading Rules:

  1. Overload signatures: Define possible function calls
  2. Implementation signature: Must be compatible with all overloads
  3. Implementation: Contains the actual logic

When to Use:

  • Same function name with different parameter combinations
  • Different return types based on input types
  • API methods with multiple usage patterns

🧩 Interfaces

Interfaces define the structure of objects. They act as a blueprint that objects must follow.

Basic Interface

interface User {
  name: string;
  email: string;
  password: string;
  gender?: string; // optional property
}

function getUser(obj: User) {
  console.log(obj.name);
  console.log(obj.gender); // might be undefined
}

getUser({
  name: "Rayied",
  email: "a@gmail.com",
  password: "1234"
});

getUser({
  name: "Rayied",
  email: "a@gmail.com",
  password: "1234",
  gender: "male",
});

βœ” Defines object shape
βœ” Optional properties with ?
βœ” Reusable across functions

Extending Interfaces

Interfaces can inherit from other interfaces using extends.

interface User {
  name: string;
  email: string;
  password: string;
  gender?: string;
}

interface Admin extends User {
  admin: boolean;
}

const createUser = (obj: User) => {
  console.log(obj.name);
};

const createAdmin = (obj: Admin) => {
  console.log(obj.email, obj.admin);
};

createUser({
  name: "Rayied",
  email: "a@gmail.com",
  password: "1234"
});

createAdmin({
  name: "Rayied",
  email: "a@gmail.com",
  password: "1234",
  admin: true,
});

βœ… Code reusability
βœ… Maintains hierarchy
βœ… Admin has all User properties + its own

Interface Merging (Declaration Merging)

Interfaces with the same name are automatically merged.

interface Person {
  name: string;
}

interface Person {
  email: string;
}

function showPerson(obj: Person) {
  console.log(obj.name);  // βœ…
  console.log(obj.email); // βœ…
}

// Person now requires both name AND email
showPerson({ name: "Ray", email: "ray@email.com" });

βœ” Useful for extending third-party libraries
⚠️ Can lead to confusion if overused


🏷️ Type Aliases

Type aliases allow you to create custom types for reusability and clarity.

Basic Type Alias

type Value = string | number | null;

let a: Value;
a = "Rayied"; // βœ…
a = 123;      // βœ…
a = null;     // βœ…
// a = true;  // ❌ Error

function processValue(val: Value) {
  console.log(val);
}

Object Type Alias

type User = {
  name: string;
  email: string;
};

function registerUser(user: User) {
  console.log(user.name);
}

Key Differences: Interface vs Type

Feature Interface Type Alias
Object shapes βœ… βœ…
Extending extends & (Intersection)
Merging βœ… Auto-merges ❌ Cannot merge
Primitives/Unions ❌ βœ…
Computed Props ❌ βœ…

When to Use:

  • Interface: For object structures, especially when you might extend them or work with classes
  • Type: For unions, intersections, primitives, or complex types

Important Note: Types cannot be redeclared:

type MyType = number;
// type MyType = string; // ❌ Error: Duplicate identifier

πŸ”— Union & Intersection Types

Union Types (|) - OR Logic

A variable can be one of multiple types.

let id: string | number;
id = "ABC123"; // βœ…
id = 101;      // βœ…
// id = true;  // ❌ Error

function printId(id: string | number) {
  console.log(id);
}

let cafe: string | null;
cafe = null;          // βœ…
cafe = "Coffee Shop"; // βœ…

βœ” Flexibility
βœ” Common for function parameters
βœ” Great for nullable values

Intersection Types (&) - AND Logic

Combines multiple types into one. The result must satisfy all types.

type User = {
  name: string;
  email: string;
};

type Admin = User & {
  getDetails(user: string): void;
};

const admin: Admin = {
  name: "Rayied",
  email: "rayied@admin.com",
  getDetails(user: string) {
    console.log(`Fetching details for ${user}`);
  },
};

βœ… Combines properties from multiple types
βœ… Great for role-based structures
βœ… Must satisfy ALL combined types


πŸ›οΈ Classes & Objects

Classes in TypeScript work like JavaScript but with type safety.

Basic Class

class Device {
  name: string = "LG";
  price: number = 12000;
  category: string = "Digital";
}

let d1 = new Device();
let d2 = new Device();
console.log(d1.name);  // LG
console.log(d2.price); // 12000

Understanding Constructors

A constructor is like a factory machine that produces objects with initial values.

Think of it as:

  • Constructor = Human Maker πŸ‘¨β€πŸ­
  • Constructor = Biscuit Maker πŸͺ
  • Constructor = Bottle Maker 🍾

It's a special method that runs automatically when you create a new instance using new.

Traditional Constructor

class Bottle {
  radius: number;
  price: number;
  color: string;

  constructor(radius: number, price: number, color: string) {
    this.radius = radius;
    this.price = price;
    this.color = color;
  }
}

let b1 = new Bottle(120, 100, "blue");
console.log(b1.color); // blue

Shorthand Constructor (Parameter Properties)

TypeScript offers a cleaner syntax using public, private, or protected directly in constructor parameters:

class BottleMaker {
  constructor(public name: string, public price: number) {}
}

let b2 = new BottleMaker("Milton", 1200);
console.log(b2.name);  // Milton
console.log(b2.price); // 1200

✨ Magic: No need to write this.name = name - TypeScript does it automatically!

Mixing Property Declarations with Constructor

class HumanMaker {
  age = 12; // default property value

  constructor(public name: string, public isHandsome: boolean) {}
}

let h1 = new HumanMaker("Rayied", true);
console.log(h1.name);       // Rayied
console.log(h1.age);        // 12
console.log(h1.isHandsome); // true

βœ… You can combine:

  • Direct property initialization (age = 12)
  • Constructor parameters (name, isHandsome)

⚠️ Constructor Parameter Rules

// ❌ WRONG: Can't use default values with public/private/protected
// class HumanMaker2 {
//   constructor(public name: string, age: number = 0) {}
// }

// βœ… CORRECT: Two ways to handle default values

// Option 1: Without access modifier
class HumanMaker2 {
  age: number;
  constructor(public name: string, age: number = 0) {
    this.age = age;
  }
}

// Option 2: Make it optional
class HumanMaker3 {
  constructor(public name: string, public age?: number) {
    this.age = age ?? 12; // default to 12 if not provided
  }
}

Key Rule: When using access modifiers (public, private, protected) in constructor parameters, you cannot use default values directly. Use optional parameters instead.

Complete Example

class Product {
  constructor(
    public name: string,
    public price: number,
    private id: string
  ) {}
}

let p1 = new Product("Laptop", 50000, "prod-123");
console.log(p1.name); // Laptop
// console.log(p1.id); // ❌ Error: private property

Access Modifiers

Access modifiers control where class properties and methods can be accessed from.

class User {
  public name: string;      // accessible everywhere (default)
  private password: string; // only inside this class
  protected email: string;  // inside class + subclasses

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

  login() {
    console.log(this.password); // βœ… accessible inside class
  }
}

class Admin extends User {
  showEmail() {
    console.log(this.email); // βœ… protected: accessible in subclass
  }
}

let user = new User("Ray", "secret123", "ray@email.com");
console.log(user.name); // βœ… public: accessible outside
// console.log(user.password); // ❌ private: not accessible outside
// console.log(user.email);    // ❌ protected: not accessible outside

Access Modifiers Summary Table

Modifier Inside Class Outside Class Subclasses (Inheritance)
public βœ… βœ… βœ…
private βœ… ❌ ❌
protected βœ… ❌ βœ…

Detailed Access Modifier Examples

Public - Access & Change Anywhere
class BottleMaker {
  constructor(public name: string) {}
}

let bt1 = new BottleMaker("Milton");
console.log(bt1.name); // βœ… Can access
bt1.name = "Cello";    // βœ… Can change
Private - Only Inside the Class
class BottleMaker2 {
  constructor(private name: string) {}

  changing() {
    this.name = "rdd"; // βœ… Can change inside the class
  }
}

class MetalBottlemaker extends BottleMaker2 {
  constructor(name: string) {
    super(name);
  }

  getValue() {
    // console.log(this.name); // ❌ Error: Cannot access private property
  }
}

let bt1 = new BottleMaker2("Milton");
// bt1.name = "hululu"; // ❌ Error: Cannot access private property
bt1.changing();         // βœ… Can call method that modifies it internally

Private Summary:

  • βœ… Can change in constructor
  • βœ… Can change in class methods
  • ❌ Cannot access in subclasses (inheritance)
  • ❌ Cannot access outside the class
Protected - Own Class + Extended Classes
class BM {
  protected name = "gg";
}

class MB extends BM {
  public material = "mt";

  changename() {
    this.name = "xd"; // βœ… Can access protected in subclass
  }
}

let pt = new MB();
// pt.name = "ff"; // ❌ Error: Cannot access protected outside
pt.changename();   // βœ… Can call method that modifies it

Protected Summary:

  • βœ… Can change in constructor
  • βœ… Can change in class methods
  • βœ… Can access in subclasses (inheritance)
  • ❌ Cannot access outside the class

Quick Reference: Access Modifiers

// public β†’ can access & change anywhere [constructor, methods, inheritance, outside]
// private β†’ can change inside class only [constructor, methods], NOT in inheritance
// protected β†’ can be used in own class AND extended class, NOT outside

Readonly Properties

The readonly modifier prevents properties from being modified after initialization.

class Car {
  readonly brand: string;
  model: string;

  constructor(brand: string, model: string) {
    this.brand = brand;
    this.model = model;
  }
}

let car = new Car("Tesla", "Model 3");
console.log(car.brand); // Tesla
// car.brand = "BMW";   // ❌ Error: readonly
car.model = "Model S";  // βœ… allowed

Readonly with Constructor Parameters

You can combine readonly with access modifiers in constructor parameters:

class User {
  constructor(public readonly name: string) {}

  changename() {
    // this.name = "new"; // ❌ Error: Cannot assign to 'name' because it is a read-only property
  }
}

let u1 = new User("hel");
console.log(u1.name); // "hel"
// u1.name = "new";   // ❌ Error: readonly

Key Points about Readonly:

  • βœ… Can be set in constructor
  • ❌ Cannot be changed in methods
  • ❌ Cannot be changed outside the class
  • Can be combined with public, private, or protected

Optional Properties

Optional properties allow you to create instances without providing all parameters. Mark them with ? in the constructor.

class Person {
  constructor(
    public name: string,
    public age: number,
    public gender?: string // optional field
  ) {}
}

let a1 = new Person("hh", 25, "male"); // βœ… with gender
let a2 = new Person("hh1", 225);       // βœ… without gender

Key Points:

  • βœ… Optional properties use ? syntax
  • βœ… Can be omitted when creating instances
  • ⚠️ Value will be undefined if not provided
  • Optional parameters must come after required ones

Parameter Properties

Parameter properties are a TypeScript shorthand that lets you declare and initialize class properties directly in the constructor parameters.

// ❌ Way 1: Traditional (Verbose)
class P1 {
  public name: string;
  public age: number;

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

// βœ… Way 2: Parameter Properties (Clean & Concise)
class P2 {
  constructor(public name: string, public age: number) {}
}

let c = new P2("alu", 22);
console.log(c.name); // "alu"
console.log(c.age);  // 22

How it Works:

  • Adding public, private, protected, or readonly in constructor parameters automatically:
    1. Declares the property
    2. Creates the property on the class
    3. Assigns the parameter value to it

Benefits:

  • βœ… Eliminates boilerplate code
  • βœ… More readable and maintainable
  • βœ… Works with all access modifiers
  • βœ… Can be combined with regular properties

Getters and Setters

Getters and setters provide controlled access to class properties. They look like properties but act like methods.

Traditional Methods (Old Way)

class P1 {
  constructor(public _name: string, public _age: number) {}

  getName() {
    return this._name;
  }

  setName(value: string) {
    this._name = value;
  }
}

// Usage: calling as methods
let person = new P1("alu", 22);
person.getName();         // ❌ Less clean
person.setName("new name"); // ❌ Less clean

Getters and Setters (Modern Way)

class P1 {
  constructor(public _name: string, public _age: number) {}

  get name() {
    return this._name;
  }

  set name(val: string) {
    this._name = val;
  }

  get age() {
    return this._age;
  }

  set age(num: number) {
    this._age = num;
  }
}

let c = new P1("alu", 22);

// Usage: access like properties, not methods
console.log(c.name); // βœ… "alu" - looks like property access
c.name = "potato";   // βœ… - looks like property assignment
console.log(c.age);  // βœ… 22
c.age = 23;          // βœ…

Benefits of Getters and Setters:

  • βœ… Cleaner syntax - access like properties (c.name) instead of methods (c.getName())
  • βœ… Validation - add logic to validate values before setting
  • βœ… Computed properties - calculate values on-the-fly
  • βœ… Encapsulation - control how properties are accessed/modified

Example with Validation:

class BankAccount {
  constructor(private _balance: number) {}

  get balance() {
    return this._balance;
  }

  set balance(amount: number) {
    if (amount < 0) {
      throw new Error("Balance cannot be negative");
    }
    this._balance = amount;
  }
}

let account = new BankAccount(1000);
console.log(account.balance); // 1000
account.balance = 2000;       // βœ… valid
// account.balance = -500;    // ❌ throws error

Convention: Use underscore prefix (_name, _age) for private/internal properties when using getters/setters.

The this Keyword

The this keyword refers to the current instance of the class.

class Example {
  name = "ray";

  changeName() {
    console.log(this.name); // refers to the instance's name property
    let a = 12;
    this.changemore = () => {
      console.log("first");
    };
  }

  changemore() {
    console.log("changing");
  }
}

βœ… this allows you to:

  • Access instance properties
  • Call other methods in the class
  • Distinguish between local variables and class properties

Static Members

Static members belong to the class itself, not to instances. You can access them without creating a new instance.

class Hero {
  static version = 1.0; // static property

  static getRandomNumber() { // static method
    return Math.random();
  }
}

// Access without creating instance
console.log(Hero.version);            // 1.0
console.log(Hero.getRandomNumber());  // random number like 0.547382

// No need for: let h = new Hero();

Key Differences:

Feature Instance Members Static Members
Belongs to Each instance The class itself
Access via instance.property ClassName.property
Requires new βœ… Yes ❌ No
Shared across all ❌ No (each has own copy) βœ… Yes (single copy)

When to Use Static Members:

  • βœ… Utility functions that don't need instance data
  • βœ… Constants shared across all instances
  • βœ… Factory methods
  • βœ… Configuration values

Example - Utility Class:

class MathHelper {
  static PI = 3.14159;

  static circleArea(radius: number) {
    return MathHelper.PI * radius * radius;
  }

  static square(num: number) {
    return num * num;
  }
}

console.log(MathHelper.PI);              // 3.14159
console.log(MathHelper.circleArea(5));   // 78.53975
console.log(MathHelper.square(4));       // 16

Abstract Classes & Methods

Abstract classes serve as base classes that cannot be instantiated directly. They provide a blueprint for other classes to extend.

Think of it like:

  • Abstract Class = Blueprint/Template (Human)
  • Concrete Classes = Actual implementations (Me, Her, Him)
// Base class with shared functionality
class Payment {
  constructor(protected amount: number, protected account: number) {}

  isPaymentValid(amount: number) {
    return this.amount > 0;
  }
}

// Specific payment implementations
class Paytm extends Payment {}
class GooglePay extends Payment {}
class PhonePe extends Payment {}

// ❌ Cannot do: new Payment(100, 123456)
// Why? Payment is a base class, not meant to be used directly

// βœ… Must use specific implementations
let payment1 = new Paytm(500, 987654);
let payment2 = new GooglePay(1000, 123456);

Why Use Abstract/Base Classes:

  • βœ… Share common functionality across related classes
  • βœ… Enforce a consistent structure
  • βœ… Avoid code duplication
  • βœ… Provide default implementations

Another Example - Cooking Essentials:

class CookingEssentials {
  constructor(protected gas: number, public gasName: string) {}
}

class Vegi extends CookingEssentials {
  cook() {
    console.log(`Cooking vegetables with ${this.gasName}`);
  }
}

class Cake extends CookingEssentials {
  bake() {
    console.log(`Baking cake with ${this.gasName}`);
  }
}

let vegDish = new Vegi(2, "LPG");
let dessert = new Cake(1, "Natural Gas");

Key Points:

  • Base classes define shared properties and methods
  • Subclasses inherit and can add their own specific features
  • Base classes typically shouldn't be instantiated directly
  • Use inheritance to create specialized versions

Real-world Analogy:

  • Payment = General concept (not used directly)
  • Paytm, GooglePay = Specific implementations (what you actually use)

🎨 Generics

Generics allow you to write reusable, type-safe code that works with multiple types.

Why Generics?

Problem without Generics:

// Using 'any' defeats TypeScript's purpose
function logger(a: any) {
  console.log(a);
}

logger("hey");
logger(12);
logger(true);
// No type safety - TypeScript can't help us

Solution with Generics:

function log<T>(val: T) {
  console.log(val);
}

log<string>("hey");   // T is string
log<number>(12);      // T is number
log<boolean>(true);   // T is boolean

Generic Functions

Generic functions use type parameters (commonly T) that act as placeholders for types.

function log<T>(val: T) {
  console.log(val);
}

function abcd<T>(a: T, b: string, c: number) {
  log(a);
}

abcd<string>("Hello", "world", 4);

How it Works:

  • <T> is a type parameter (like a variable for types)
  • When you call abcd<string>(...), T becomes string
  • TypeScript enforces that a must be a string

Type Inference:

// TypeScript can infer the type automatically
log("hello");  // T inferred as string
log(42);       // T inferred as number

Generic Interfaces

Generics make interfaces flexible and reusable.

interface Food<T> {
  name: string;
  age: number;
  key: T;
}

function foods<T>(obj: Food<T>) {
  log(obj);
  log(obj.key);
}

foods<string>({ name: "foo", age: 25, key: "Asas" });
foods<number>({ name: "bar", age: 30, key: 123 });

Benefits:

  • βœ… Single interface works with multiple types
  • βœ… Type safety maintained
  • βœ… Reusable across different data structures

Generic Classes

Classes can use generics to create type-safe, flexible components.

class BottleMaker<T> {
  constructor(public key: T) {}
}

let b1 = new BottleMaker<string>("hey");
let b2 = new BottleMaker<number>(12);
console.log(b1, b2);

Real-world Example:

class DataStore<T> {
  private data: T[] = [];

  add(item: T): void {
    this.data.push(item);
  }

  get(index: number): T {
    return this.data[index];
  }
}

let numberStore = new DataStore<number>();
numberStore.add(1);
numberStore.add(2);

let stringStore = new DataStore<string>();
stringStore.add("hello");
stringStore.add("world");

Generic Constraints

Sometimes you need to restrict what types can be used with generics.

function c<T>(a: T, b: T): T {
  // return a;           // βœ… no error
  // return "hey";       // ❌ error
  // return b;           // βœ… no error
  // return "hey" as T;  // βœ… works (type assertion)
  return "hey" as T;     // βœ… also works
}

c<string>("hey", "hello");

Type Narrowing in Generics:

function processValue<T>(value: T): T {
  // TypeScript doesn't know what T is yet
  // value.toUpperCase(); // ❌ Error: Property doesn't exist on type T

  // Use type narrowing
  if (typeof value === "string") {
    value.toUpperCase(); // βœ… Now TypeScript knows it's a string
  }

  return value;
}

Advanced Constraints:

// Constrain T to have a 'length' property
function getLength<T extends { length: number }>(item: T): number {
  return item.length;
}

getLength("hello");      // βœ… string has length
getLength([1, 2, 3]);    // βœ… array has length
// getLength(123);       // ❌ number doesn't have length

Multiple Type Parameters

You can use multiple generic type parameters.

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

let result1 = pair<string, number>("age", 25);
let result2 = pair<boolean, string>(true, "yes");

When to Use Generics

βœ… Use Generics When:

  • Creating reusable components/functions
  • Working with collections (arrays, maps, sets)
  • Building libraries or frameworks
  • Type depends on caller's input

❌ Don't Use Generics When:

  • Type is always the same
  • Only used in one place
  • Adds unnecessary complexity

πŸ“¦ Modules

Modules help organize code into separate files and control what is shared between them.

Exporting and Importing

Named Exports

payment.ts:

export function addPayment(val: number) {
  console.log(val);
}

export function getDetails() {
  console.log("Getting details");
}

export const TAX_RATE = 0.15;

app.ts:

import { addPayment, getDetails, TAX_RATE } from "./payment";

addPayment(12);
getDetails();
console.log(TAX_RATE);

Default Exports

payment.ts:

export default class Payment {
  constructor(public price: number) {}

  processPayment() {
    console.log(`Processing payment of ${this.price}`);
  }
}

app.ts:

import Payment from "./payment";

let payment = new Payment(12);
payment.processPayment();
console.log(payment.price);

Mixing Named and Default Exports

payment.ts:

export default class Payment {
  constructor(public price: number) {}
}

export function addPayment(val: number) {
  console.log(val);
}

export const TAX_RATE = 0.15;

app.ts:

import Payment, { addPayment, TAX_RATE } from "./payment";

let p = new Payment(100);
addPayment(12);

Import Aliases

import { addPayment as processPayment } from "./payment";

processPayment(12); // Same as addPayment(12)

Namespace Imports

import * as PaymentUtils from "./payment";

PaymentUtils.addPayment(12);
PaymentUtils.getDetails();
console.log(PaymentUtils.TAX_RATE);

Module Best Practices

βœ… One module per file
βœ… Use named exports for utilities
βœ… Use default exports for main class/component
βœ… Keep module dependencies minimal
βœ… Use barrel exports for cleaner imports

Barrel Export Example:

index.ts:

export { Payment } from "./payment";
export { User } from "./user";
export { Product } from "./product";

Usage:

import { Payment, User, Product } from "./index";

🎭 Type Assertions

Type assertion is used when you know more about a variable or value than TypeScript does.

Basic Type Assertion

let a: any = 12;

// Method 1: Using 'as' syntax (preferred)
(a as number).toFixed(2);

// Method 2: Using angle bracket syntax (not in JSX/TSX)
(<number>a).toFixed(2);

Common Use Cases

DOM Manipulation

// TypeScript doesn't know the specific element type
let input = document.getElementById("username");
// input.value; // ❌ Error: Property 'value' doesn't exist on HTMLElement

// Tell TypeScript it's an input element
let input = document.getElementById("username") as HTMLInputElement;
input.value = "John"; // βœ… Works

API Responses

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

async function getUser() {
  let response = await fetch("/api/user");
  let data = await response.json(); // type: any

  // Assert the type
  let user = data as User;
  console.log(user.name); // βœ… TypeScript knows the structure
}

Type Assertion vs Type Casting

Type Assertion (TypeScript only - compile-time):

let a: any = "12";
(a as number); // Doesn't actually convert, just tells TypeScript to treat it as number

Type Casting (JavaScript - runtime):

let a = Number("12");  // Actually converts string to number
console.log(a);        // 12
console.log(typeof a); // "number"

Non-null Assertion Operator (!)

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

let b: null | undefined | string;
b = "hey";

// Without ! - all methods shown
b.toUpperCase; // ❌ TypeScript warns: possibly null/undefined

// With ! - guarantees it's not null/undefined
b!.toUpperCase(); // βœ… All string methods available

⚠️ Use Sparingly:

  • Only when you're absolutely certain the value exists
  • Commonly used with DOM elements and optional chaining
  • Bypasses TypeScript's safety checks

Example:

function processUser(user?: User) {
  // ❌ Dangerous if user might be undefined
  console.log(user!.name);

  // βœ… Better approach
  if (user) {
    console.log(user.name);
  }
}

Type Assertion Best Practices

βœ… Use type assertions when:

  • You have more information than TypeScript
  • Working with third-party libraries
  • Dealing with DOM elements
  • Handling API responses

❌ Avoid type assertions when:

  • You're not sure about the actual type
  • TypeScript's inference is correct
  • You can use type guards instead

Better Alternatives:

// ❌ Using type assertion
let value = getUserInput() as string;

// βœ… Using type guard
let value = getUserInput();
if (typeof value === "string") {
  // TypeScript knows it's a string here
  console.log(value.toUpperCase());
}

πŸ›‘οΈ Type Guards

Type guards help TypeScript narrow down types safely at runtime.

Using typeof

The typeof operator checks primitive types.

function abcd(arg: string | number | boolean) {
  if (typeof arg === "number") {
    // arg is number here
    return arg.toFixed(2);
  } else if (typeof arg === "string") {
    // arg is string here
    return arg.toUpperCase();
  } else {
    // arg is boolean here
    return arg ? "true" : "false";
  }
}

console.log(abcd(12));      // "12.00"
console.log(abcd("hello")); // "HELLO"
console.log(abcd(true));    // "true"

typeof Returns:

  • "string" for strings
  • "number" for numbers
  • "boolean" for booleans
  • "object" for objects, arrays, null
  • "function" for functions
  • "undefined" for undefined

Using instanceof

The instanceof operator checks if an object is an instance of a class.

class TvRemote {
  switchTVoff() {
    console.log("turn off TV");
  }
}

class CarRemote {
  switchCaroff() {
    console.log("turn off car");
  }
}

const tv = new TvRemote();
const car = new CarRemote();

function switchedoff(device: TvRemote | CarRemote) {
  if (device instanceof TvRemote) {
    device.switchTVoff(); // TypeScript knows it's TvRemote
  } else if (device instanceof CarRemote) {
    device.switchCaroff(); // TypeScript knows it's CarRemote
  }
}

switchedoff(tv);  // "turn off TV"
switchedoff(car); // "turn off car"

Custom Type Guards

You can create your own type guard functions using type predicates.

interface Fish {
  swim: () => void;
}

interface Bird {
  fly: () => void;
}

// Custom type guard function
function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

function moveAnimal(animal: Fish | Bird) {
  if (isFish(animal)) {
    animal.swim(); // TypeScript knows it's Fish
  } else {
    animal.fly();  // TypeScript knows it's Bird
  }
}

Type Predicate: pet is Fish

  • Returns true if pet is a Fish
  • TypeScript uses this to narrow the type

in Operator

Check if a property exists in an object.

type Admin = {
  name: string;
  privileges: string[];
};

type User = {
  name: string;
  email: string;
};

function printInfo(person: Admin | User) {
  console.log(person.name);

  if ("privileges" in person) {
    console.log(person.privileges); // TypeScript knows it's Admin
  }

  if ("email" in person) {
    console.log(person.email); // TypeScript knows it's User
  }
}

Discriminated Unions

Use a common property to distinguish between types.

interface Circle {
  kind: "circle";
  radius: number;
}

interface Square {
  kind: "square";
  sideLength: number;
}

type Shape = Circle | Square;

function getArea(shape: Shape) {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.sideLength ** 2;
  }
}

Benefits:

  • βœ… Exhaustive checking
  • βœ… Clear intent
  • βœ… TypeScript helps catch missing cases

Type Guards Comparison

Type Guard Use Case Example
typeof Primitive types typeof x === "string"
instanceof Class instances x instanceof MyClass
in Object properties "prop" in obj
Custom Complex logic function isType(x): x is Type
Discriminated Union Tagged types switch (x.kind)

πŸ› οΈ TypeScript Utility Types

TypeScript provides built-in utility types to transform existing types.

Partial<T>

Makes all properties optional.

interface User {
  name: string;
  email: string;
  age: number;
}

function updateUser(user: User, updates: Partial<User>) {
  return { ...user, ...updates };
}

let user: User = { name: "John", email: "john@email.com", age: 30 };
let updated = updateUser(user, { age: 31 }); // Only update age

Required<T>

Makes all properties required (opposite of Partial).

interface Config {
  host?: string;
  port?: number;
  debug?: boolean;
}

// Force all properties to be required
let config: Required<Config> = {
  host: "localhost", // βœ… must provide
  port: 3000,        // βœ… must provide
  debug: true        // βœ… must provide
};

Readonly<T>

Makes all properties read-only.

interface User {
  name: string;
  email: string;
}

let user: Readonly<User> = {
  name: "John",
  email: "john@email.com"
};

// user.name = "Jane"; // ❌ Error: Cannot assign to 'name' because it is read-only

Pick<T, K>

Creates a type by picking specific properties from another type.

interface User {
  id: number;
  name: string;
  email: string;
  password: string;
  age: number;
}

// Only pick name and email
type UserPreview = Pick<User, "name" | "email">;

let preview: UserPreview = {
  name: "John",
  email: "john@email.com"
  // id, password, age are not needed
};

Omit<T, K>

Creates a type by omitting specific properties (opposite of Pick).

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

// Omit password
type PublicUser = Omit<User, "password">;

let publicUser: PublicUser = {
  id: 1,
  name: "John",
  email: "john@email.com"
  // password is excluded
};

Record<K, T>

Creates an object type with keys of type K and values of type T.

type Role = "admin" | "user" | "guest";

let permissions: Record<Role, string[]> = {
  admin: ["read", "write", "delete"],
  user: ["read", "write"],
  guest: ["read"]
};

Utility Types Summary

Utility Type Purpose Example
Partial<T> Make all props optional Partial<User>
Required<T> Make all props required Required<Config>
Readonly<T> Make all props read-only Readonly<User>
Pick<T, K> Select specific props Pick<User, "name" | "email">
Omit<T, K> Exclude specific props Omit<User, "password">
Record<K, T> Create key-value object Record<string, number>

🎯 Complete Summary

Core Concepts

  • TypeScript = JavaScript + Type Safety
  • Catches errors at compile time, not runtime
  • Better IDE support with autocomplete and IntelliSense

Type System

  • Primitives: number, string, boolean
  • Arrays: number[], string[]
  • Tuples: Fixed-length typed arrays [number, string]
  • Enums: Named constants
  • Special: any, unknown, void, never, null, undefined

Advanced Types

  • Interfaces β†’ Define object shapes (can extend & merge)
  • Type Aliases β†’ Create custom types (unions, intersections)
  • Union (|) β†’ One of several types
  • Intersection (&) β†’ Combination of multiple types

Functions

  • Function Types β†’ Define parameter and return types
  • Optional Parameters β†’ Use ? for optional params
  • Default Parameters β†’ Provide fallback values
  • Rest Parameters β†’ Accept variable number of arguments
  • Function Overloading β†’ Multiple signatures for same function

Generics

  • Generic Functions β†’ Reusable type-safe functions
  • Generic Interfaces β†’ Flexible interface definitions
  • Generic Classes β†’ Type-safe, reusable classes
  • Constraints β†’ Restrict generic types

Object-Oriented Programming

  • Classes β†’ OOP with type safety
  • Access Modifiers:
    • public β†’ accessible everywhere
    • private β†’ only within the class
    • protected β†’ within the class and subclasses
  • Constructors β†’ Initialize instances
  • Parameter Properties β†’ Shorthand for declaring properties
  • Optional Properties β†’ Use ? for optional fields
  • Readonly Properties β†’ Prevent modifications after initialization
  • Getters/Setters β†’ Controlled property access
  • Static Members β†’ Belong to class, not instances
  • Inheritance β†’ extends keyword
  • Abstract/Base Classes β†’ Templates for other classes

Modules

  • Named Exports β†’ Export multiple items
  • Default Exports β†’ Export single main item
  • Import Aliases β†’ Rename imports
  • Namespace Imports β†’ Import everything

Type Safety

  • Type Assertions β†’ Tell TypeScript about types
  • Type Casting β†’ Convert between types at runtime
  • Non-null Assertion β†’ Guarantee non-null/undefined
  • Type Guards β†’ Narrow types safely
    • typeof β†’ Check primitive types
    • instanceof β†’ Check class instances
    • in β†’ Check object properties
    • Custom type guards β†’ Complex type checking

Utility Types

  • Partial β†’ Make properties optional
  • Required β†’ Make properties required
  • Readonly β†’ Make properties immutable
  • Pick β†’ Select specific properties
  • Omit β†’ Exclude specific properties
  • Record β†’ Create key-value types

Best Practices

βœ… Use unknown instead of any
βœ… Prefer interfaces for object shapes
βœ… Use type aliases for unions/intersections
βœ… Always add return types to functions
βœ… Use readonly for immutable properties
βœ… Leverage type inference where obvious
βœ… Use protected when you need inheritance access
βœ… Use private for truly internal-only properties
βœ… Use parameter properties to reduce boilerplate
βœ… Use optional properties (?) instead of null when possible
βœ… Use getters/setters for controlled property access
βœ… Use static members for utility functions and constants
βœ… Use generics for reusable, type-safe code
βœ… Use type guards instead of type assertions when possible
βœ… Organize code with modules
βœ… Leverage utility types to transform types


πŸ“š Additional Resources


Happy Coding with TypeScript! πŸš€

Made with πŸ’™ by MD. SHAHZAD HUSSAIN RAYIED

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors