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.
- 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
- Requires compilation (
.ts β transpiled β .js) - Enforces disciplined coding (stricter syntax)
# Install TypeScript globally
npm install -g typescripttsc --initThis generates a tsconfig.json file for compiler settings.
tsc app.ts # compile manually
tsc --watch # auto-compile on file changenumberstringbooleannullundefinedobject
anyunknownneverenumtuple
let id: number = 101;
let username: string = "Alice";
let isActive: boolean = true;let nums: number[] = [1, 2, 3, 4];
let mixed: (number | string)[] = [1, "two", 3]; // union type arraylet 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.
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
let data: any = 123;
data = "hello";
data = true;
// data.toUpperCase(); // No error shown, but might fail at runtimeany - it defeats the purpose of TypeScript!
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
function logMessage(): void {
console.log("No return value");
}
function getStatus(): boolean {
return true; // must return boolean
}let cafe: string | null = null;
cafe = "Starbucks"; // β
// cafe = 12; // β Error
let uninitialized: undefined;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"); // β οΈ Unreachablelet a = 12;
let b = a;
b = 20;
console.log(a); // 12 (unchanged)
console.log(b); // 20let 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
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
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 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
}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";
}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) => voidmeans:- Function takes one parameter of type
string - Returns nothing (
void)
- Function takes one parameter of type
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 optionalDefault 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 beundefined - Default (
=): Parameter has a fallback value
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:
...arrcollects 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 copyFunction 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 signatureFunction Overloading Rules:
- Overload signatures: Define possible function calls
- Implementation signature: Must be compatible with all overloads
- 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 define the structure of objects. They act as a blueprint that objects must follow.
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
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
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
Type aliases allow you to create custom types for reusability and clarity.
type Value = string | number | null;
let a: Value;
a = "Rayied"; // β
a = 123; // β
a = null; // β
// a = true; // β Error
function processValue(val: Value) {
console.log(val);
}type User = {
name: string;
email: string;
};
function registerUser(user: User) {
console.log(user.name);
}| 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 identifierA 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
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 in TypeScript work like JavaScript but with type safety.
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); // 12000A 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.
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); // blueTypeScript 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!
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)
// β 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.
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 propertyAccess 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| Modifier | Inside Class | Outside Class | Subclasses (Inheritance) |
|---|---|---|---|
public |
β | β | β |
private |
β | β | β |
protected |
β | β | β |
class BottleMaker {
constructor(public name: string) {}
}
let bt1 = new BottleMaker("Milton");
console.log(bt1.name); // β
Can access
bt1.name = "Cello"; // β
Can changeclass 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 internallyPrivate Summary:
- β Can change in constructor
- β Can change in class methods
- β Cannot access in subclasses (inheritance)
- β Cannot access outside the class
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 itProtected Summary:
- β Can change in constructor
- β Can change in class methods
- β Can access in subclasses (inheritance)
- β Cannot access outside the class
// 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 outsideThe 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"; // β
allowedYou 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: readonlyKey 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, orprotected
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 genderKey Points:
- β
Optional properties use
?syntax - β Can be omitted when creating instances
β οΈ Value will beundefinedif not provided- Optional parameters must come after required ones
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); // 22How it Works:
- Adding
public,private,protected, orreadonlyin constructor parameters automatically:- Declares the property
- Creates the property on the class
- 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 provide controlled access to class properties. They look like properties but act like methods.
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 cleanclass 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 errorConvention: Use underscore prefix (_name, _age) for private/internal properties when using getters/setters.
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 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)); // 16Abstract 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 allow you to write reusable, type-safe code that works with multiple types.
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 usSolution 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 booleanGeneric 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>(...),Tbecomesstring - TypeScript enforces that
amust be a string
Type Inference:
// TypeScript can infer the type automatically
log("hello"); // T inferred as string
log(42); // T inferred as numberGenerics 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
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");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 lengthYou 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");β 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 help organize code into separate files and control what is shared between them.
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);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);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 { addPayment as processPayment } from "./payment";
processPayment(12); // Same as addPayment(12)import * as PaymentUtils from "./payment";
PaymentUtils.addPayment(12);
PaymentUtils.getDetails();
console.log(PaymentUtils.TAX_RATE);β
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 assertion is used when you know more about a variable or value than TypeScript does.
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);// 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"; // β
Worksinterface 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 (TypeScript only - compile-time):
let a: any = "12";
(a as number); // Doesn't actually convert, just tells TypeScript to treat it as numberType Casting (JavaScript - runtime):
let a = Number("12"); // Actually converts string to number
console.log(a); // 12
console.log(typeof a); // "number"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- 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);
}
}β 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 help TypeScript narrow down types safely at runtime.
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
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"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
trueif pet is a Fish - TypeScript uses this to narrow the type
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
}
}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 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 provides built-in utility types to transform existing types.
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 ageMakes 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
};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-onlyCreates 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
};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
};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 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> |
- TypeScript = JavaScript + Type Safety
- Catches errors at compile time, not runtime
- Better IDE support with autocomplete and IntelliSense
- Primitives: number, string, boolean
- Arrays:
number[],string[] - Tuples: Fixed-length typed arrays
[number, string] - Enums: Named constants
- Special:
any,unknown,void,never,null,undefined
- Interfaces β Define object shapes (can extend & merge)
- Type Aliases β Create custom types (unions, intersections)
- Union (
|) β One of several types - Intersection (
&) β Combination of multiple types
- 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
- Generic Functions β Reusable type-safe functions
- Generic Interfaces β Flexible interface definitions
- Generic Classes β Type-safe, reusable classes
- Constraints β Restrict generic types
- Classes β OOP with type safety
- Access Modifiers:
publicβ accessible everywhereprivateβ only within the classprotectedβ 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 β
extendskeyword - Abstract/Base Classes β Templates for other classes
- Named Exports β Export multiple items
- Default Exports β Export single main item
- Import Aliases β Rename imports
- Namespace Imports β Import everything
- 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 typesinstanceofβ Check class instancesinβ Check object properties- Custom type guards β Complex type checking
- 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
β
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
- Official TypeScript Docs
- TypeScript Playground - Test TS code in browser
- TypeScript Deep Dive Book - Free comprehensive guide
- TypeScript Cheat Sheet
Happy Coding with TypeScript! π
Made with π by MD. SHAHZAD HUSSAIN RAYIED