- TypeScript is a superset of JavaScript that introduces static typing, meaning you can define the types of variables, function parameters, and return values. This helps catch errors early in the development process, leading to more robust and maintainable code, especially in large projects.
- It's developed by Microsoft. TypeScript code gets compiled (transpiled) to regular JavaScript, which browsers understand.
- TypeScript files use
.tsextension. - TypeScript provides extra capabilities by adding type to JavaScript.
- Consider below code
function add(a, b) {
return a + b;
}
console.log(add(2, 3)); // 5
console.log(add("2", 3)); // "23" — unexpected!
- JS doesn't stop you from passing wrong data types.
- Consider below code
let user = { name: "Alex" };
console.log(user.age(25)); // Runtime error!
Output:
ERROR!
/tmp/SINf75RlXo/main.js:2
console.log(user.age(25)); // Runtime error!
^
TypeError: user.age is not a function
- Without types, it's harder to refactor and maintain code.
- Type Safety
- Better Autocompletion and IntelliSense
- Compile-time error checking
- Helps with large codebases and team projects
- Consider below example
- TypeScript's static typing helps catch type-related errors during development, preventing runtime errors. By explicitly defining types, code becomes easier to understand and maintain, especially in larger projects.
- TypeScript uses compile time checking which means it checks if the specified type matches or the object consist of required attributes before running the code.
Note
- Before installing
TypeScriptensure you have installednode.js
- Run the below command to install TypeScript
npm install -g typescript
- Verify the version
tsc -v
- Let's script a
.tsfile.
function add(a: number, b: number): number {
return a + b;
}
console.log(add(2, 3)); // ✅ Works
//console.log(add("2", 3)); // ❌ Error during compilation , TypeScript will warn: Argument of type 'string' is not assignable to parameter of type 'number'.
let Obj={
name:"John",
age:30,
isStudent:false
}
console.log(Obj.name); // ✅ Work
console.log(Obj.age);
- Now we need to compile the
.tsfile, this will generate a new javascript file with same name.
- Let's run the
sample.jsusing node usingnode sample.jsor via browser by creating a HTML file.
// sample.js
function add(a, b) {
return a + b;
}
console.log(add(2, 3)); // ✅ Works
//console.log(add("2", 3)); // ❌ Error during compilation
var Obj = {
name: "John",
age: 30,
isStudent: false
};
console.log(Obj.name); // ✅ Work
console.log(Obj.age);
Output:
5
John
30
- TypeScript offers explicit and implicit ways to assign types to variables.
- This method involves writing the type explicitly after the variable name, using a colon
:
let variableName: type = value;
- Example
let firstName: string = "John";
- If a type is not explicitly specified, TypeScript attempts to infer the type based on the assigned value
let message = "Hello, TypeScript!"; // TypeScript infers 'message' as type string
let count = 100; // TypeScript infers 'count' as type number
- If we try to change the value of variable to a different type. It shows compilation error.
- If TypeScript cannot infer the type, it defaults to
any, which disables type checking for that variable.
let count; // Type is implicitly any
count = 10;
count = "abc"
console.log(count);
Output:
abc
- In TypeScript, types are the building blocks that help you describe what kind of data you're working with.
- These are the basic types just like in JavaScript but with explicit typing.
let username: string = "Alice";
let age: number = 25;
let floatValues: number = 1.45;
let isActive: boolean = true;
let greeting: string = `Hello, ${username}! You are ${age} years old.`;
let nothing: null = null;
let notDefined: undefined = undefined;
let justInitialize;
console.log(typeof username); // string
console.log(typeof age); // number
console.log(typeof isActive); // boolean
console.log(typeof greeting); // string
console.log(typeof floatValues); // number
console.log(typeof nothing); // object (this is a quirk in JavaScript)
/*
The output object when using typeof null in JavaScript is a well-known quirk and considered a historical bug.
In the initial implementation of JavaScript, values were stored with a type tag.
The value null was represented with all bits set to zero, which, by a flaw in the type-checking logic,
was interpreted as the object type tag.
*/
console.log(typeof notDefined); // undefined
console.log(typeof justInitialize); // undefined
anyskips the type checking.
let anyType: any = "This can be anything";
anyType = 42; // Now it's a number
anyType = true; // Now it's a boolean
anyType = { name: "Alice" }; // Now it's an object
console.log(typeof anyType); // object
if(anyType){
console.log("anyType is truthy");
}
nullrepresents the intentional absence of any object value. It's a value you can explicitly assign to a variable to signify that it currently holds no object reference. In TypeScript,nullis considered falsy.undefinedrepresents the absence of a value, especially when a variable has been declared but not yet assigned a value. It can also be the return value of a function that doesn't explicitly return a value. In TypeScript,undefinedis considered falsy.anyit is a type that disables type checking in TypeScript, allowing a variable to hold any value, including primitives, objects, or even other types. It's a way to bypass the static type checking features of TypeScript.anyis truthy.
![WARNING]
- Avoid using using
anyas type, because your code changes will act like normal javascript.
- In TypeScript, array types define the structure of an array, specifying the same type of elements it can hold.
- There are two main ways to declare array types
- Using square brackets
[]
let arr_numbers: number[] = [1, 2, 3];
let arr_strings: string[] = ["hello", "world"];
let arr_booleans: boolean[] = [true, false, true];
console.log(typeof arr_numbers); // object (arrays are objects in JavaScript)
console.log(arr_numbers.length); // 3
console.log(arr_strings[0]); // "hello"
console.log(arr_booleans[1]); // false
console.log(arr_numbers[1]); // 2
arr_numbers.push(4); // Adding a number to the array
console.log(arr_numbers); // [1, 2, 3, 4]
- Using the
Array<T>generic type - This approach uses the built-in Array interface with a type parameter T to specify the element type.
let numbers: Array<number> = [1, 2, 3];
let strings: Array<string> = ["hello", "world"];
let booleans: Array<boolean> = [true, false, true];
We will learn about interface later
- One can delete element using following ways.
splice()modifies the original array by removing or replacing elements. It takes the index of the element to be removed and the number of elements to remove as arguments.
let arr: number[] = [1, 2, 3, 4, 5];
arr.splice(2, 1); // Removes 1 element at index 2 (value 3)
console.log(arr); // Output: [1, 2, 4, 5]
filter()creates a new array containing elements that pass a test implemented by a provided function. It's useful for removing elements based on a condition.
let arr: number[] = [1, 2, 3, 4, 5];
let newArr = arr.filter(item => item !== 3); // Creates a new array without the value 3
console.log(newArr); // Output: [1, 2, 4, 5]
console.log(arr); // Output: [1, 2, 3, 4, 5] (original array unchanged)
slice()returns a shallow copy of a portion of an array into a new array object selected from start to end (end not included) where start and end represent the index of items in that array. The original array will not be modified.
let arr: number[] = [1, 2, 3, 4, 5];
let newArr = arr.slice(0,2).concat(arr.slice(3));
console.log(newArr) // Output: [1, 2, 4, 5]
deletewhiledelete arr[index]can remove an element, it leaves a "hole" (undefined value) in the array and does not update the array's length, which is generally not desirable.
let arr: number[] = [1, 2, 3];
delete arr[1]; // Removes the element at index 1
console.log(arr); // Output: [1, <1 empty item>, 3]
- These are arrays of arrays, and their types are defined by nesting array type declarations. It's used to represent data structures with more than one dimension, such as matrices or tables. Each nested array represents a row, and the elements within those arrays represent columns
// Declaring a 2D array (matrix) of numbers
let matrix: number[][] = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
];
// Accessing elements in a 2D array
let element = matrix[1][2]; // Accesses the element at row 1, column 2 (value: 6)
// Declaring a 3D array
let cube: number[][][] = [
[
[1, 2],
[3, 4],
],
[
[5, 6],
[7, 8],
],
];
- In TypeScript, a
readonlyarray is an array whose elements cannot be modified after it's created. This means you cannot add, remove, or change elements of the array. It's useful for ensuring data immutability and preventing unintended modifications.
const readonly_names: readonly string[] = ["Alice", "Bob"];
// readonly_names[0] = "Charlie"; // Error: Index signature in type 'readonly string[]' only permits reading.
const readonly_numbers: ReadonlyArray<number> = [1, 2, 3];
// readonly_numbers.push(4); // Error: Property 'push' does not exist on type 'readonly number[]'
// Using as const assertion
const data = [10, 20, 30] as const;
data.push(40); // Error: Property 'push' does not exist on type 'readonly [10, 20, 30]'
- In TypeScript, a
readonlyarray with aconstassertion ensures that the array's elements and the array itself cannot be modified after initialization.constassertion tells the compiler to infer the most specific type possible, including making the array readonly and inferring literal types for its elements. - When you declare an array with as
const, TypeScript infers atupletype withreadonlymodifiers for each element. This means you cannot use methods that would modify the array, such as push, pop, or assigning new values to existing indexes.
const numbers = [1, 2, 3] as const;
// numbers.push(4); // Error: Property 'push' does not exist on type 'readonly [1, 2, 3]'.
// numbers[0] = 10; // Error: Cannot assign to '0' because it is a read-only property.
// Correct way to use the array
const firstNumber = numbers[0]; // Accessing elements is allowed
- You as a developer tells TypeScript Hey TypeScript, trust me. I know what this is..
- Type assertion in TypeScript allows developers to override the type inferred by the compiler and explicitly specify a different type for a value. It serves as a way to tell the compiler trust me, I know what I'm doing when you have more information about the type of a value than TypeScript can infer.
- Type assertions can be performed using two syntaxes:
assyntax or<>bracket syntax
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
- Another example
const titleElement = document.getElementById("title") as HTMLHeadingElement;
// Now TypeScript knows it's a heading, so we can do:
titleElement.innerText = "Hello, TypeScript!";
titleElement.style.color = "blue";
const boxElement = <HTMLDivElement> document.getElementById("box");
boxElement.style.backgroundColor = "lightgreen";
boxElement.innerHTML = "<p>This is a box!</p>";
- You're telling TypeScript Hey, trust me — this element is an HTMLHeadingElement (like
<h1>) - What assertion does NOT do:
- It doesn’t check anything at runtime.
- It won’t throw an error if you’re wrong.
- What if type asserted is wrong?
let value: unknown = 123;
// You're telling TypeScript:
let str = value as string;
console.log(str.length); // ❌ Works in TypeScript, but at runtime:
// number has no 'length', so it can fail
let someValue: any = null;
let myString = someValue as string; // TS says OK
console.log(myString.length); // 💥 Runtime crash: Cannot read 'length' of null
- TypeScript is not magical. It doesn’t check at runtime — it only helps at compile-time.
- If you use wrong assertions, the compiler won’t complain — but the browser might crash when running it.
- These define arrays with a fixed number of elements, where each element can have a different type. Tuples are fixed number of elements with known types.
let person: [string, number] = ["Alice", 30];
console.log(person[0]); // "Alice"
console.log(person[1]); // 30
person[0] = "Bob"; // Changing the name
person[1] = 35; // Changing the age
console.log(person); // ["Bob", 35]
- If we add elements in tuple like below
person.push("extra"); // This will won't cause an error
console.log(person); // ["Bob", 35, "extra"]
- While it appears possible to use the
.push()method on a TypeScript tuple, it's generally not recommended due to the nature of tuples. - Tuples are designed to have a fixed length and specific types at each index, and using
.push()can bypass these constraints. If you use the.push()method on a tuple, TypeScript won't throw a compile-time error, but it can lead to unexpected behavior and type inconsistencies. - The added element won't be type-checked and will effectively make the tuple behave like a regular array.
- To maintain the integrity of tuples, it's better to avoid using
.push(). If you need to add elements, consider creating a new tuple with the desired elements or using an array if the fixed-length constraint is not necessary.
- In TypeScript, the type keyword serves to define the shape of data, acting as a way to create aliases for data types. The
typekeyword in TypeScript is used to define a custom name for a type.typelets you create a reusable label for a complex or frequently used type. - Type Aliases allow defining types with a custom name (an Alias).
- Without
type, you'd have to rewrite long type annotations everywhere. That gets messy.
let person_name_age: [string, number] = ["John Doe", 30];
let person_name_age2: [string, number] = ["Jane Doe", 25];
let person_name_age3: [string, number] = ["Alice", 28];
let person_name_age4: [string, number] = ["Bob", 35];
let person_name_age5: [string, number] = ["Charlie", 40];
let person_name_age6: [string, number] = ["Dave", 45];
let person_name_age7: [string, number] = ["Eve", 50];
- This is repetitive and hard to maintain.
- Now using
typewe can reuse, maintain clean code and can update easily
type personDetails = [string, number];
let person_name_age: personDetails = ["John Doe", 30];
let person_name_age1: personDetails = ["Harsh", 25];
let person_name_age2: personDetails = ["Alice", 28];
let person_name_age3: personDetails = ["Bob", 35];
let person_name_age4: personDetails = ["Charlie", 22];
let person_name_age5: personDetails = ["David", 40];
let person_name_age6: personDetails = ["Eve", 29];
let person_name_age7: personDetails = ["Frank", 33];
- We can give alias to primitive types, function, object etc..
type UserID = string;
let id: UserID = "abc123";
type Person = {
name: string;
age: number;
};
let person: Person = { name: "Jane Doe", age: 25 };
type Point = {
x: number;
y: number;
};
let point: Point = { x: 10, y: 20 };
type MathOp = (a: number, b: number) => number;
const add: MathOp = (a, b) => a + b;
- In TypeScript, a function type defines the types of parameters a function accepts and the type of value it returns. It ensures that functions are called with the correct arguments and produce the expected output, contributing to type safety.
function greet(name: string): string {
return "Hello " + name;
}
let add: (a: number, b: number) => number = (a, b) => a + b;
console.log(greet("Harsh"))
console.log(add(5,6))
Output:
Hello Harsh
11
- You can also pass function as a parameter
// Define a function type: takes two numbers and returns a number
let add: (a: number, b: number) => number = (x, y) => x + y;
let multiply: (a: number, b: number) => number = (x, y) => x * y;
// This function accepts another function as a parameter
function calculate(
operation: (a: number, b: number) => number,
x: number,
y: number
): number {
return operation(x, y);
}
// Use it
console.log(calculate(add, 5, 3)); // ➝ 8
console.log(calculate(multiply, 5, 3)); // ➝ 15
- You can make it cleaner by defining a
typefor the function or providing alias making it reusable.
type BinaryOperation = (a: number, b: number) => number;
let subtract: BinaryOperation = (a, b) => a - b;
function calculate(operation: BinaryOperation, x: number, y: number): number {
return operation(x, y);
}
console.log(calculate(subtract, 10, 4)); // ➝ 6
- You can also pass a function inline or anonymous function (no need to define
add/subtract/multiplyseparately).
function calculate(operation: (a: number, b: number) => number, x: number, y: number): number {
return operation(x, y);
}
console.log(
calculate((a, b) => a / b, 10, 2) // ➝ 5
);
- TypeScript also allows you to return a function from a function
type returnedFunction = (a: number, b: number) => number; // Define Return Function arguments and return type
function getOperation(type: string): returnedFunction {
if (type === "add") return (a, b) => a + b;
return (a, b) => a - b;
}
const op = getOperation("add");
console.log(op(7, 2)); // ➝ 9
- In TypeScript,
voidis a keyword that represents the absence of a return value from a function. It's used to indicate that a function does not return any meaningful data. When a function is declared with avoidreturn type, it means that the function performs some actions or side effects but doesn't produce a value that needs to be returned to the caller.
function greet(name: string): void {
console.log(`Hello, ${name}!`);
}
greet("World"); // This function doesn't return anything
- It's important to note that while a
voidfunction doesn't explicitly return a value, it might still implicitly returnundefinedin JavaScript. However, TypeScript treats void as distinct fromundefinedin the context of function return types.
function logMessage(message: string): void {
console.log(message);
// return undefined; // Optional, but redundant
}
function getValue(): undefined {
return undefined; // Explicitly returns undefined
}
let myVariable: undefined = undefined;
voidis used to denote that a function does not return any value. It indicates the absence of a return value. However, a function declared asvoidcan still technically return a value, but that value will be ignored. Even if you addreturn;orreturn undefined;, it still counts asvoid. You're saying - I’m not expecting any value when I call this function.
function logMessage(message: string): void {
console.log(message);
return;
}
console.log(logMessage("Hi"));
Output:
Hi
undefined
undefined— a real value that means "nothing is set". You use it when you expect a variable to possibly have no value.
let name: undefined = undefined; // ✅ valid
function getNothing(): undefined {
return undefined;
}
- You are saying - I will return something, and that something is undefined.
// Function that returns nothing (void)
function logSomething(msg: string): void {
console.log(msg);
// return; // ✅ allowed
// return undefined; // ✅ also allowed, but not needed
}
// Function that MUST return undefined
function returnNothing(): undefined {
return undefined; // ✅ MUST return undefined explicitly
}
- Another example of
voidusing anonymous function.
let logMessage: (message: string) => void = (msg) => console.log(msg);
- In TypeScript, you can have optional parameters in functions — they let you call a function without passing every single argument. Use a
?after the parameter name.
function greetAll(name: string, age?: number) {
if (age) {
console.log(`Hello ${name}, you are ${age} years old.`);
} else {
console.log(`Hello ${name}!`);
}
}
greetAll("Alice", 25); // Hello Alice, you are 25 years old.
greetAll("Bob"); // Hello Bob!
ageis optional. You can call the function with or without it.- Behind the Scenes when a parameter is marked optional, TypeScript treats it as
age?: number // same as → age: number | undefined
- So you can check like this inside the function
if (age !== undefined) { ... }
- Optional parameters must come after required ones
function doSomething(a: string, b?: string) {} // ✅ OK
function doSomethingWrong(a?: string, b: string) {} // ❌ Error
- You can also give default values (acts like optional too)
function greet(name: string, age: number = 18) {
console.log(`${name} is ${age} years old.`);
}
greet("Tom"); // Tom is 18 years old
- Rest parameters allow a function to accept any number of arguments as an array.
function sum(...numbers: number[]): number {
return numbers.reduce((acc, n) => acc + n, 0);
}
console.log(sum(5, 10, 15)); // 30
- When you don’t know how many arguments will be passed. When you want to collect multiple values into one variable. If a function will have required, optional, default and rest parameters then
- ✅ Required parameters come first
- ✅ Then optional parameters (
?) - ✅ Then default parameters (
=) - ✅ Finally, rest parameters (
...args) — it must be last.
function example(
id: number, // required
name?: string, // optional
city: string = "Unknown", // default
...hobbies: string[] // rest
): void {
console.log("ID:", id);
console.log("Name:", name);
console.log("City:", city);
console.log("Hobbies:", hobbies);
}
example(1);
// Output:
// ID: 1
// Name: undefined
// City: Unknown
// Hobbies: []
example(2, "Alice");
// ID: 2
// Name: Alice
// City: Unknown
// Hobbies: []
example(3, "Bob", "Pune", "Reading", "Gaming");
// ID: 3
// Name: Bob
// City: Pune
// Hobbies: ['Reading', 'Gaming']
- TypeScript tries to "guess" the return type of a function if you don’t tell it what the return type should be. This is called type inference — TypeScript infers the return type based on the
returnstatements inside the function. - Consider below example
function add(a: number, b: number) {
return a + b;
}
- TypeScript infers
number. Here,a + bis anumber. So, TypeScript understands,addreturns anumber. So inferred return type isnumber. - Consider below example
function greet(name: string) {
return `Hello, ${name}!`;
}
- Return value is a
string. So,greetis understood to return astring. So inferred return type isstring. - Consider below example
function logMessage(message: string) {
console.log(message);
}
- No
returnstatement here. So TypeScript says ➤ Okay, this function doesn’t return anything useful. Inferred return type isvoid. voidis the default return type if the function does not havereturnkeyword specified.- Consider below example
function mixedTypes(flag: boolean) {
if (flag) {
return "Hello";
} else {
return 123;
}
}
- One return is a
string. One return is anumber. So TypeScript makes a union typestring | number. Inferred return type isstring | number - Consider below example
function noReturnType() {
// nothing returned
}
- No return and no type. So TypeScript says I don't know what this is → gives it the
anytype - This means it can be anything — ❌ not safe &
⚠️ not good! - Even though TypeScript is smart, it's a good habit to explicitly add return types, especially for bigger or important functions.
function add(a: number, b: number): number {
return a + b;
}
- In TypeScript, object types let you define the shape of an object. Alongside
string,numberandboolean, it's one of the most commonly used annotations in a TypeScript codebase.
let user: { name: string; age: number } = {
name: "Alice",
age: 25
};
console.log(user.name); // ✅ "Alice"
- You can use object type into function as well
type User = {
name: string;
isActive: boolean;
};
function greetings(user: User): string {
return `Hello ${user.name}, active: ${user.isActive}`;
}
console.log(greetings({ name: "Alice", isActive: true }));
Output:
Hello Alice, active: true
- In TypeScript, a union type allows a variable to hold values of multiple types. Essentially, it expresses that a value could be one of several types.
- Union types are created using the vertical bar (
|) to separate the types. For example,string | numbermeans a variable can hold either a string or a number. - Consider below example
let id: string | number;
id = 123; // ✅ number
id = "ABC123"; // ✅ string
id = true; // ❌ Error: boolean not allowed
- You can use union with functions,
type, arrays etc..
function printId(id: string | number): void | string {
console.log("Your ID is:", id);
}
printId(101); // ✅
printId("A102"); // ✅
printId(true); // ❌
type Status = "success" | "error" | "loading";
let currentStatus: Status;
currentStatus = "success"; // ✅
currentStatus = "error"; // ✅
currentStatus = "pending"; // ❌ Not part of union
// These arrays can hold elements of different types, specified using a union type.
let mixed: (number | string)[] = [1, "hello", 2, "world"];
-
You can union:
- primitive types (
string|number) - custom types (
User|Admin) - literal values (
yes|no) null/undefinedfor optional-like behavior
- primitive types (
-
Some more examples
let userName: string | null;
userName = "Alice"; // ✅
userName = null; // ✅
// Define custom types
type Admin = {
name: string;
role: "admin";
accessLevel: number;
};
type Customer = {
name: string;
role: "customer";
loyaltyPoints: number;
};
// Union type of Admin or Customer
type AnyUser = Admin | Customer;
// Example function
function getUserInfo(user: AnyUser) {
console.log("Name:", user.name);
if (user.role === "admin") {
console.log("Access Level:", user.accessLevel);
} else {
console.log("Loyalty Points:", user.loyaltyPoints);
}
}
// ✅ Sample usage
const adminUser: AnyUser = {
name: "Alice",
role: "admin",
accessLevel: 5
};
const customerUser: AnyUser = {
name: "Bob",
role: "customer",
loyaltyPoints: 120
};
getUserInfo(adminUser);
getUserInfo(customerUser);
- Example of union in multi-dimensional array
let multiArr: (string | number)[][] = [
["apple", 1],
["banana", 2],
["cherry", 3]
];
- In this example,
multiArris a 2D array where each element can be either a string or a number. - Example of union with tuples
let multiArr2: ([string, number] | [boolean, string])[] = [
["apple", 1],
[true, "banana"],
["cherry", 3]
];
- Here,
multiArr2is a 1D array where each element is either a tuple of[string, number]or[boolean, string]. - Be careful, when using union types, TypeScript doesn't know which one you're using unless you check..
function format(value: string | number):void {
// TypeScript doesn't know if it's string or number
if (typeof value === "string") {
console.log(value.toUpperCase()); // ✅ ok
} else {
console.log(value.toFixed(2)); // ✅ ok
}
}
- In TypeScript, intersection types allow you to combine multiple types into a single type. This resulting type has all the properties of the constituent types.
- It is denoted using the
&operator. - Let's see an example
type Person = { name: string };
type Employee = { employeeId: number };
type Staff = Person & Employee;
let staffMember: Staff = {
name: "Alice",
employeeId: 101
};
- An intersection type combines multiple types into one. It means, the final value must satisfy all types joined with
&.
let invalidPerson: Staff = {
name: "Bob"
// Missing age ❌
};
- More example
type Location = { city: string };
type Contact = { phone: string };
type Employee = HasName & HasAge & Location & Contact;
const emp: Employee = {
name: "John",
age: 30,
city: "Mumbai",
phone: "1234567890"
};
- Example of union and intersection
// Union (either/or)
type A = { name: string };
type B = { age: number };
type C = A | B;
let val: C = { name: "Alice" }; // ✅ okay
val = { age: 22 }; // ✅ okay
val = { name: "Bob", age: 22 }; // ✅ okay
// Intersection (both)
type D = A & B;
let val2: D = { name: "Alice", age: 22 }; // ✅ must have both
- In TypeScript, a literal type is a type that represents a specific, fixed value. It allows you to specify that a variable or parameter must have one particular value, instead of a broader type like
stringornumberorboolean - It basically restrict a variable to exact values.
// String literal type
let greeting: "hello" = "hello";
// greeting = "hi"; // Error: Type '"hi"' is not assignable to type '"hello"'
// Numeric literal type
let magicNumber: 42 = 42;
// magicNumber = 43; // Error: Type '43' is not assignable to type '42'
// Boolean literal type
let isEnabled: true = true;
// isEnabled = false; // Error: Type 'false' is not assignable to type 'true'
// Union of literal types
type Status = "success" | "error" | "pending";
let requestStatus: Status = "success";
requestStatus = "error";
// requestStatus = "failed"; // Error: Type '"failed"' is not assignable to type '"success" | "error" | "pending"'
let direction: "left" | "right" | "up" | "down";
direction = "left"; // ✅
direction = "forward"; // ❌ Error
- Other example
type EmailLocaleIDs = "welcome_email" | "email_heading";
type FooterLocaleIDs = "footer_title" | "footer_sendoff";
type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;
// type AllLocaleIDs = "welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id"
-
A function or expression that never returns or can’t ever happen.
-
🚫 Use when:
-
A function throws an error or
-
A function has an infinite loop
-
A variable can never have a value
-
-
If we don't define anything, the function may return
anyorvoid. Soneverhelps TypeScript catch impossible code paths, and ensures your function doesn’t accidentally return something.
function error(message: string): never {
throw new Error(message);
}
function infiniteLoop(): never {
while (true) {}
}
- Functions that always throw an error or enter an infinite loop never produce a return value. In these cases, never is the appropriate return type.
- Now consider below code.
type Shape = 'circle' | 'square';
function getArea(shape: Shape): number {
switch (shape) {
case 'circle':
return Math.PI;
case 'square':
return 10;
default:
const _exhaustiveCheck: never = shape;
return _exhaustiveCheck;
}
}
- In this example, if a new
Shapetype is added (likerectangle) and theswitchstatement isn't updated, TypeScript will produce an error because the_exhaustiveCheckvariable will receive a value that isn't of typenever. This helps catch potential bugs. neverenhances type safety by allowing TypeScript to enforce constraints on code that should never be reached or functions that should never return.- It helps catch potential errors and ensures that all possible cases are handled in conditional logic.
- The
unknowntype in TypeScript is a type-safe counterpart to theanytype. It represents any value, just likeany, but with stricter type checking. - Let's consider an example of
any.
let data: any;
data = "hello";
data.toUpperCase(); // ✅ Works
data = 123;
data.toUpperCase(); // ❌ Runtime error! Because numbers don’t have toUpperCase
- Here, ➡️ TypeScript won’t warn you, but your app will crash at runtime 😱
- Now let's see an example of
unknown
let data: unknown;
data = "hello";
// ❌ Error: Object is of type 'unknown'
// data.toUpperCase();
if (typeof data === "string") {
// ✅ Safe: TypeScript now knows it's a string
console.log(data.toUpperCase());
}
data = 123;
if (typeof data === "number") {
console.log(data + 10); // ✅ Safe
}
- TypeScript forces you to check the type — which avoids runtime crashes 🔐
- This, can be really helpful suppose you are fetching a data from an API.
function fetchData(): unknown {
// Simulate API response
return JSON.parse('{ "name": "Alice", "age": 30 }');
}
let result = fetchData();
// ❌ Not allowed: result.name (because it's unknown)
if (typeof result === "object" && result !== null && "name" in result) {
// ✅ Now we can safely access
console.log((result as { name: string }).name);
}
- If this were
any, you might write
let result: any = fetchData();
console.log(result.name.toUpperCase()); // Might crash if name doesn't exist
any= 🤷♂️ Trust me, I know what I’m doing — no checksunknown= 🔐 I don't know what this is yet, let's check before using it- TypeScript’s goal = catch bugs before they happen — and
unknownsupports that unknownprovides flexibility without losing type safety. Forces you to check the type first before using it. Perfect for handling user inputs, external data, etc.
- An
enum(short for enumeration) is a special type in TypeScript used to define a set of named constants. It helps you write readable and organized code when you have a known list of values. - Let's see an example
enum Direction {
North,
East,
South,
West
}
let dir: Direction = Direction.North;
console.log(Direction[0]); // Output: North
console.log(dir); // Output: 0 (by default, enums start at 0)
- Lets say you wanted to assign api error codes.
enum StatusCode {
Success = 200,
NotFound = 404,
ServerError = 500
}
let code = StatusCode.Success;
console.log(code); // 200
- You can also use enums in functions
enum Role {
Admin,
User,
Guest
}
function checkAccess(role: Role) {
if (role === Role.Admin) {
console.log("Full access");
} else {
console.log("Limited access");
}
}
checkAccess(Role.User); // Output: Limited access
- Enums group related constants. It improve readability. Avoid repetition. Get autocompletion + type safety.
- An interface in TypeScript is a way to describe the shape of an object — meaning what properties and types it should have.
- It’s like saying - Any object of this type must have these properties.
- Let's see an example
interface Person {
name: string;
age: number;
}
let p: Person = {
name: "Alice",
age: 25
};
- Here, the
Personinterface. EveryPersonobject must have aname(string) and anage(number). - You can pass optional parameters as well
interface Product {
name: string;
price: number;
discount?: number; // optional
}
let item: Product = {
name: "Shoes",
price: 1200
};
- You can extend interface as well
interface Animal {
name: string;
}
interface Dog extends Animal {
breed: string;
}
const myDog: Dog = {
name: "Max",
breed: "Labrador"
};
- Let's consider
typeexamples
// Type Example
type Person = {
name: string;
age: number;
};
type Employee = Person & { employeeId: string };
typecan represent object shapes, can do uniontype Result = Success | Failure, can alias primitivestype ID = number, can use intersections but it cannot do declaration merging meaning it means you can define an interface multiple times, and TypeScript will automatically merge them into one like below
interface User {
name: string;
}
interface User {
age: number;
}
const person: User = {
name: "Alice",
age: 25
};
- ❌
typecannot do this, example withtype(this will throw an error)
type User = {
name: string;
};
type User = {
age: number;
}; // ❌ Error: Duplicate identifier 'User'
- A type cannot be changed after being created
- Type aliases cannot be declared more than once. You can do intersection but with
interface, it looks cleaner when you're modeling real-world entities, especially when you want to:- Extend multiple interfaces
- Use in OOP-style classes
- Allow others to extend via declaration merging
- If you want to annotate the return type of a function which returns a promise, you should use the
Promisetype.
function getDataFromServer(): Promise<string> {
return new Promise((resolve) => {
setTimeout(() => {
resolve("Here is your data!");
}, 2000);
});
}
// Using the function with .then()
getDataFromServer().then((data) => {
console.log(data); // Output after 2 sec: "Here is your data!"
});
- With
async/await
function getDataFromServer(): Promise<string> {
return new Promise((resolve) => {
setTimeout(() => {
resolve("Here is your data!");
}, 2000);
});
}
async function fetchData() {
const result = await getDataFromServer();
console.log(result); // "Here is your data!" after 2 seconds
}
fetchData()
Promise<string>means this function will eventually return a string, not right away.- In TypeScript, the return type of a
Promiseis denoted asPromise<T>, whereTrepresents the type of the value that thePromisewill resolve to. If a function is expected to return a Promise that resolves to astring, its return type annotation would bePromise<string>. If the Promise resolves to anumber, the return type would bePromise<number>, and so on. If the Promise does not resolve to any value, the return type isPromise<void>.
// Promise that resolves to a string
function getStringPromise(): Promise<string> {
return new Promise((resolve) => {
setTimeout(() => {
resolve("Hello, world!");
}, 1000);
});
}
// Promise that resolves to a number
function getNumberPromise(): Promise<number> {
return new Promise((resolve) => {
setTimeout(() => {
resolve(42);
}, 1500);
});
}
// Promise that does not resolve to any value
function getVoidPromise(): Promise<void> {
return new Promise((resolve) => {
setTimeout(() => {
resolve();
}, 2000);
});
}
- If the return type is not explicitly defined, TypeScript might infer it as
Promise<unknown>by default, which can lead to issues when working with the resolved value later on.
- TypeScript has a few rare but real types besides
string,number,boolean.
bigint– For very large numbers
let bigNumber: bigint = BigInt(1234567890123456789012345678901234567890);
symbol– Unique and constant values, often for object keys
let sym1 = Symbol("desc");
let sym2 = Symbol("desc");
console.log(sym1 === sym2); // false (even though same description)
- Generics in TypeScript enable the creation of reusable components that can work with a variety of data types while maintaining type safety. They allow you to define functions, classes, or interfaces with placeholder types, which are then specified when the component is used. This eliminates the need to write multiple versions of the same code for different types, reducing code duplication and improving maintainability.
// Generic function
function identity<T>(arg: T): T {
return arg;
}
let myString: string = identity<string>("hello");
let myNumber: number = identity<number>(123);
// Generic interface
interface KeyValuePair<K, V> {
key: K;
value: V;
}
let pair1: KeyValuePair<string, number> = { key: "one", value: 1 };
let pair2: KeyValuePair<number, string> = { key: 2, value: "two" };
function firstElement<T>(arr: T[]): T {
return arr[0];
}
firstElement<string>(["a", "b"]); // returns "a"
- It is helpful to write reusable functions, types, or classes and keeps type safety without hardcoding types
- In TypeScript,
keyofis a keyword that extracts the key type from an object type, producing a union of string or number literal types representing the keys of that object. It enables compile-time type checking for property access, ensuring that you only access valid properties.
interface Person {
name: string;
age: number;
}
type PersonKeys = keyof Person; // "name" | "age"
const person: Person = {
name: "John Doe",
age: 30,
};
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
getProp({ name: "Alice", age: 25 }, "name"); // returns "Alice"
- Indexed access types in TypeScript allow the lookup of a specific property's type on another type. This mechanism uses the syntax similar to accessing object properties in JavaScript, but instead of retrieving a value, it retrieves the type of that value.
interface Person {
name: string;
age: number;
}
type PersonName = Person["name"]; // string
type PersonAge = Person["age"]; // number
- Indexed access types can also be used with unions,
keyof, or other types to access properties dynamically or conditionally.
type Keys = keyof Person; // "name" | "age"
type ValueByName = Person[Keys]; // string | number
type Accessor = "name" | "age";
type ValueByAccessor = Person[Accessor]; // string | number
- Indexed access types can be used to extract types from arrays and tuples as well.
type Numbers = number[];
type FirstNumber = Numbers[0]; // number
type Tuple = [string, number, boolean];
type SecondElement = Tuple[1]; // number
- Conditional types in TypeScript enable the definition of type transformations based on conditions, operating similarly to a ternary operator at the type level. The syntax follows the structure
T extends U ? X : Y, where if typeTis assignable to typeU, the resulting type isX; otherwise, it'sY. These types facilitate enforcing constraints, extracting properties' types, and writing reusable code. - Syntax
T extends U ? X : Y
- Example
type IsString<T> = T extends string ? "Yes" : "No";
type A = IsString<string>; // "Yes"
type B = IsString<number>; // "No"
type NonNullable<T> = T extends null | undefined ? never : T;
type StringOrNumber = string | number;
type OnlyString = Extract<StringOrNumber, string>; // string
- In the
NonNullableexample, ifTextendsnullorundefined, the type resolves tonever; otherwise, it retains the original typeT. - Now consider below code
type ElementType<T> = T extends (infer U)[] ? U : T
type A = ElementType<number[]>; // number ✅ (because it's an array of number)
type B = ElementType<string>; // string ✅ (not an array, so just string)
- It says that
type ElementType<T> = T extends (infer U)[] ? U : T- IfTis an array of something, give me that something (element type). Otherwise, just give meT. - The
inferkeyword in TypeScript is used within conditional types to deduce types. It allows the extraction of a type from a related type structure.infercan only be used inside theextendsclause of a conditional type and is commonly used to extract return types of functions, element types of arrays, or parameter types of constructors. - Consider another example
type Flatten<T> = T extends Array<infer Item> ? Item : T
type A = Flatten<string[]>; // string
type B = Flatten<number[][]>; // number[]
type C = Flatten<boolean>; // boolean
-
type Flatten<T> = T extends Array<infer Item> ? Item : T, this is exactly the same idea - IfTis an array, give me the item type, else just giveT. -
Consider another example
type ToArray<T> = T extends any ? T[] : never
type StringOrNumberArray = ToArray<string | number>;
// type StringOrNumberArray = string[] | number[];
T = string | number. TypeScript checks -string extends any ? string[] : never, then -number extends any ? number[] : never. Similarly forstringas well
- Let's create a simple class
class Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
greet(): void {
console.log(`Hi, my name is ${this.name} and I'm ${this.age} years old.`);
}
}
const p = new Person("Alice", 30);
p.greet();
constructorruns when you create a new object.thisrefers to the current instance. You must declare property types (e.g.,name: string) in TypeScript.- TypeScript adds access control to class members:
| Modifier | Description |
|---|---|
| public | (default) Accessible anywhere |
| private | Only accessible within the class |
| protected | Accessible within the class and its subclasses |
- Example
class Animal {
public name: string;
private age: number;
protected type: string;
constructor(name: string, age: number, type: string) {
this.name = name;
this.age = age;
this.type = type;
}
describe() {
console.log(`${this.name} is a ${this.type}`);
}
}
const a = new Animal("Dog", 5, "Mammal");
// a.age -> ❌ Error (private)
// a.type -> ❌ Error (protected)
- A class can extend another class and inherit its properties and methods.
class Employee extends Person {
jobTitle: string;
constructor(name: string, age: number, jobTitle: string) {
super(name, age); // call parent constructor
this.jobTitle = jobTitle;
}
work() {
console.log(`${this.name} works as a ${this.jobTitle}`);
}
}
const e = new Employee("Bob", 28, "Developer");
e.greet(); // Inherited from Person
e.work(); // Defined in Employee
- You can make properties
read-onlyso they can't be changed after initialization.
class Book {
readonly title: string;
constructor(title: string) {
this.title = title;
}
print() {
console.log(this.title);
}
}
const book = new Book("TypeScript Handbook");
// book.title = "New Title"; ❌ Error
- You can control access to properties using
getandset.
class Temperature {
private _celsius: number = 0;
get fahrenheit(): number {
return this._celsius * 1.8 + 32;
}
set fahrenheit(value: number) {
this._celsius = (value - 32) / 1.8;
}
}
const temp = new Temperature();
temp.fahrenheit = 98.6;
console.log(temp.fahrenheit); // 98.6
- Use
interfaceto define the shape of objects. Useclassto implement behavior.
interface Logger {
log(msg: string): void;
}
class ConsoleLogger implements Logger {
log(msg: string) {
console.log(msg);
}
}
- Narrowing means TypeScript figures out the exact type of a variable at a certain point in your code based on checks you do — like
typeof,if, or others. - Imagine a variable could be a
string | number, and inside anifstatement you check if it's a string — now TypeScript knows it's a string in that block.
function printLength(value: string | number) {
if (typeof value === "string") {
console.log("Length:", value.length); // ✅ value variable is now treated as string
} else {
console.log("Value doubled:", value * 2); // ✅ value variable is now treated as number
}
}
- This is type narrowing in action.
- Used for primitive types like
string,number,boolean,symbol.
function handleInput(input: string | number) {
if (typeof input === "string") {
console.log("Upper:", input.toUpperCase()); // string
} else {
console.log("Doubled:", input * 2); // number
}
}
- Used for classes or object instances.
class Car {
drive() {
console.log("Driving car");
}
}
class Bike {
ride() {
console.log("Riding bike");
}
}
function move(vehicle: Car | Bike) {
if (vehicle instanceof Car) {
vehicle.drive(); // ✅ narrowed to Car
} else {
vehicle.ride(); // ✅ narrowed to Bike
}
}
- Check if a property exists on an object.
type Dog = { bark: () => void };
type Cat = { meow: () => void };
function speak(animal: Dog | Cat) {
if ("bark" in animal) {
animal.bark(); // ✅ must be a Dog
} else {
animal.meow(); // ✅ must be a Cat
}
}
- Based on the value is truthy or falsy
function getUsersOnlineMessage(numUsersOnline: number) {
if (numUsersOnline) {
return `There are ${numUsersOnline} online now!`;
}
return "Nobody's here. :(";
}
- A Type Guard is a function or condition that tells TypeScript what a variable’s type is within a certain block of code.
- TypeScript then "narrows" the type — so you don’t have to manually cast or assert it.
function isString(val: unknown): val is string {
return typeof val === "string";
}
function handle(val: unknown) {
if (isString(val)) {
console.log("It's a string:", val.toUpperCase());
} else {
console.log("Not a string:", val);
}
}
- val is string is a type predicate — it tells TypeScript trust me, it's a string here.
- TypeScript can narrow types based on comparison operations, like
===,!==, etc.
function processInput(input: "yes" | "no" | boolean) {
if (input === "yes") {
console.log("User said YES");
} else if (input === true) {
console.log("Boolean TRUE");
} else {
console.log("Other:", input);
}
}
- Even though input is a
unionof strings and boolean, TypeScript narrows the type based on your if checks.
- A Discriminated Union is a pattern where different types in a union share a common property (usually a
kindortypestring field). TypeScript uses this field to discriminate between types and narrow accordingly.
type Circle = {
kind: "circle";
radius: number;
};
type Square = {
kind: "square";
side: number;
};
type Shape = Circle | Square;
function getArea(shape: Shape) {
if (shape.kind === "circle") {
return Math.PI * shape.radius ** 2; // TypeScript knows it's a Circle
} else {
return shape.side * shape.side; // TypeScript knows it's a Square
}
}
- The
kindproperty helps TypeScript narrow down the type automatically — no need forinstanceofor extra checks. Instead ofkindyou can have your own property to check
type Circle = {
property: "circle";
radius: number;
};
type Square = {
property: "square";
side: number;
};
type Shape = Circle | Square;
function getArea(shape: Shape) {
if (shape.property === "circle") {
return Math.PI * shape.radius ** 2; // TypeScript knows it's a Circle
} else {
return shape.side * shape.side; // TypeScript knows it's a Square
}
}
let obj: Shape = {
property: "square",
side: 4
}
console.log(getArea(obj)) //16
- Let's dive deep into how TypeScript works under the hood, step by step, with a complete example. We’ll explore how TypeScript compiles (or more precisely, transpiles) to JavaScript, and what really happens when your
.tscode runs. - TypeScript is a superset of JavaScript — it adds types and modern features (like enums, interfaces, generics) to plain JS.
- It is not executed directly by the browser or Node.js.
- TypeScript must be compiled/transpiled to regular JavaScript before it can run.
- TypeScript is a superset of JavaScript. It adds static typing, interfaces, enums, generics, and type checking, but in the end, all TypeScript code must be transformed into JavaScript — because browsers and Node.js don’t understand
.tsfiles. - Transpiler means transform + compiler. TypeScript is technically a transpiler because it transforms TypeScript code to JavaScript. It also does compilation because it checks types at compile time, and reports errors before the code runs.
- It uses the TypeScript Compiler (
tsc), which:- Checks types
- Removes type annotations
- Converts modern features into compatible JavaScript
- TypeScript does not interpret or execute code directly — the output JavaScript is what gets executed.
- Let's take a code example,
greet.ts
function greet(name: string): string {
return `Hello, ${name}`;
}
const result = greet("Alice");
console.log(result);
- You run
tsc greet.ts - TypeScript:
- Checks the types (name must be a
string, etc.) and checks for type errors (like if you try to assign a string to a number). - Removes type annotations (
: string). Ignores type annotations when generating JS (they don’t exist at runtime). - Outputs JavaScript
greet.js
- Checks the types (name must be a
"use strict";
function greet(name) {
return "Hello, " + name;
}
var result = greet("Alice");
console.log(result);
- As you can see:
- The types are gone (they’re only for development safety).
- TypeScript transforms syntax where necessary to match the configured JS version (like ES5, ES6, etc.).
- All type annotations are removed (like
name: string). - The result is pure, valid JavaScript.
- The resulting JavaScript (
greet.js) is run by a JS runtime engine, like:- Chrome's V8
- Node.js -
node greet.js
- Your
.tsfile is parsed into an AST (Abstract Syntax Tree). This structure allows TypeScript to understand code structure. - Then it does type checking based on your type annotations (or inferred types), TypeScript checks:
- Are the function parameters valid?
- Are the variable types consistent?
- Do return types match?
- If errors exist, they’re shown at compile time, not at runtime.
- Once type checks pass, TypeScript generates JS code by stripping out type information.
- TypeScript can handle modern features that JavaScript engines might not support yet (based on your
tsconfig.jsonconfig file) - It means:
- JavaScript keeps evolving—new features (like optional
chaining ?., nullish coalescing ??,etc.) get added over time. - But older browsers or JavaScript engines might not support those features yet.
- TypeScript can understand those new features and then convert (or "transpile") them into older JavaScript code so they work everywhere.
- JavaScript keeps evolving—new features (like optional
- This is controlled by your TypeScript config file
tsconfig.json. tsconfig.jsonconfiguration file that tells the TypeScript compiler how to compile your .ts files.
Note
- If you don't see
tsconfig.jsonfile you can generate it usingtsc --init.





