TypeScript โ Complete Beginner Tutorial
6 modules ยท 18 examples ยท Click โถ Run Code to see the console output
๐ก How to use this tutorial
Read each explanation, study the code, then click โถ Run Code to see the output.
Types & Variables
TypeScript adds type annotations to JavaScript. A type annotation tells TypeScript what kind of data a variable holds โ string, number, boolean, etc. If you try to put the wrong kind of data in a variable, TypeScript shows an error immediately in your editor, before you even run the code. This is the core benefit of TypeScript.
Primitive Types โ string, number, boolean
The three most common types are string (text), number (any number โ integer or decimal), and boolean (true or false). You add the type after the variable name with a colon. TypeScript can also infer the type from the value you assign โ so you do not always have to write it explicitly.
// Explicit type annotations
let firstName: string = "Alice";
let age: number = 25;
let isStudent: boolean = true;
let score: number = 98.5;
// TypeScript infers types automatically
let city = "Cape Town"; // TypeScript knows this is string
let count = 10; // TypeScript knows this is number
let active = false; // TypeScript knows this is boolean
// You cannot assign the wrong type
// city = 42; // โ Error: Type 'number' is not assignable to 'string'
// age = "twenty"; // โ Error: Type 'string' is not assignable to 'number'
// Printing values
console.log("Name:", firstName);
console.log("Age:", age);
console.log("City:", city);
console.log("Student:", isStudent);
console.log("Score:", score);TypeScript's type inference is very smart. In practice, most developers let TypeScript infer the type when it is obvious (like let name = "Alice") and only write explicit annotations when the type is not clear from the value, or when declaring a variable without a value.
Union Types and null
A union type means a variable can be one of several types. You write them with the | (pipe) symbol. This is very useful when a value might be a string OR a number, or when something might be null. TypeScript forces you to handle all possible types before using the value.
// Union type โ can be string OR number
let id: string | number = "user_123";
console.log("ID (string):", id);
id = 456; // This is also valid!
console.log("ID (number):", id);
// Nullable โ can be string OR null
let username: string | null = null;
console.log("Username before login:", username);
username = "alice";
console.log("Username after login:", username);
// Function with union type
function formatId(id: string | number): string {
if (typeof id === "string") {
return id.toUpperCase(); // safe โ we checked it's a string
}
return id.toString(); // safe โ we know it's a number
}
console.log(formatId("abc123"));
console.log(formatId(789));
// Optional chain โ safe access that might be null
let user: { name: string } | null = null;
console.log(user?.name ?? "No user found");The typeof check inside an if statement is called a "type guard". After the if (typeof id === "string") check, TypeScript knows id is definitely a string inside that block, so it allows string methods like .toUpperCase(). This is TypeScript's "narrowing" feature.
Arrays and Tuples
An array in TypeScript holds multiple values of the same type. Write the type followed by [] to declare an array. A tuple is like an array but with a fixed number of elements where each position has a specific type. Tuples are useful for returning multiple values from a function.
// Typed arrays
let names: string[] = ["Alice", "Bob", "Charlie"];
let scores: number[] = [95, 87, 92, 78];
let flags: boolean[] = [true, false, true];
// Array methods work the same as JavaScript
names.push("Diana");
console.log("Names:", names);
console.log("First score:", scores[0]);
console.log("Total names:", names.length);
// Generic array syntax (alternative way)
let languages: Array<string> = ["TypeScript", "Python", "Rust"];
console.log("Languages:", languages.join(", "));
// Tuple โ fixed structure, each slot has its own type
let person: [string, number, boolean] = ["Alice", 25, true];
console.log("Name:", person[0]);
console.log("Age:", person[1]);
console.log("Active:", person[2]);
// Destructuring a tuple
const [name, userAge, isActive] = person;
console.log(`${name} is ${userAge} years old.`);string[] and Array<string> mean exactly the same thing. Most TypeScript developers prefer string[] as it is shorter to type. Use Array<string> when the type is complex or when you want to be extra clear about what the array contains.
Functions & Types
Functions in TypeScript have typed parameters and a typed return value. This means TypeScript knows exactly what data goes in and what data comes out. If you pass the wrong type of argument or forget to return the right type, TypeScript catches it immediately. Use void as the return type when a function does not return anything.
Typed Parameters and Return Types
Write the type of each parameter after its name with a colon. Write the return type after the closing parenthesis with a colon. TypeScript will check that every return statement in the function actually returns the correct type.
// Function with typed params and return type
function add(a: number, b: number): number {
return a + b;
}
function greet(name: string, role: string): string {
return `Hello, ${name}! You are a ${role}.`;
}
// void โ function returns nothing
function logMessage(message: string): void {
console.log("[LOG]", message);
}
// Arrow functions โ same typing rules
const multiply = (x: number, y: number): number => x * y;
const shout = (text: string): string => text.toUpperCase() + "!!!";
// Using the functions
console.log(add(15, 27));
console.log(greet("Alice", "developer"));
logMessage("Server started");
console.log(multiply(6, 7));
console.log(shout("hello typescript"));If you forget to add a return type, TypeScript will still infer it from what you return. But writing explicit return types is good practice for public functions โ it makes your code self-documenting and catches mistakes where you accidentally return undefined.
Optional and Default Parameters
An optional parameter has a ? after its name โ the caller does not have to provide it. A default parameter has a = value โ if the caller does not provide it, the default is used. Optional parameters must come after required ones.
// Optional parameter with ?
function introduce(name: string, age?: number): string {
if (age !== undefined) {
return `${name} is ${age} years old.`;
}
return `Name: ${name}`;
}
console.log(introduce("Alice", 25));
console.log(introduce("Bob")); // age is optional
// Default parameter
function createTag(
text: string,
tag: string = "p",
className: string = ""
): string {
const cls = className ? ` class="${className}"` : "";
return `<${tag}${cls}>${text}</${tag}>`;
}
console.log(createTag("Hello"));
console.log(createTag("Title", "h1"));
console.log(createTag("Card", "div", "card-body"));
// Rest parameters โ accept any number of args
function sumAll(...numbers: number[]): number {
return numbers.reduce((total, n) => total + n, 0);
}
console.log(sumAll(1, 2, 3));
console.log(sumAll(10, 20, 30, 40, 50));Default parameters are usually better than optional parameters because you always have a value to work with โ no need to check for undefined. Use optional parameters (?) when no default makes sense and the absence of the value has real meaning in your logic.
Objects & Interfaces
An interface in TypeScript defines the shape of an object โ what properties it has and what types those properties are. Think of it as a contract: any object that claims to be a User must have all the properties the User interface defines. Interfaces are one of the most important features in TypeScript and you will use them constantly.
Defining and Using Interfaces
Use the interface keyword to define an object shape. Properties with ? are optional. Properties with readonly cannot be changed after they are set. Once you define an interface, TypeScript ensures every object of that type has all the required properties with the correct types.
// Define an interface
interface User {
id: number;
name: string;
email: string;
age?: number; // optional โ does not have to be provided
readonly role: string; // cannot be changed after creation
}
// Create objects that match the interface
const alice: User = {
id: 1,
name: "Alice Smith",
email: "alice@example.com",
age: 25,
role: "admin",
};
const bob: User = {
id: 2,
name: "Bob Jones",
email: "bob@example.com",
// age is optional โ fine to omit
role: "user",
};
// Function that accepts the interface
function displayUser(user: User): void {
console.log(`[${user.role.toUpperCase()}] ${user.name}`);
console.log(` Email: ${user.email}`);
if (user.age !== undefined) {
console.log(` Age: ${user.age}`);
}
}
displayUser(alice);
console.log("---");
displayUser(bob);readonly is not the same as const. const prevents the variable from being reassigned. readonly prevents a property inside an object from being changed. Use readonly for things like IDs and creation timestamps that should never change after being set.
Extending Interfaces
Interfaces can extend other interfaces to inherit their properties. This lets you build up complex types from simpler building blocks without repeating yourself. A class can also implement an interface, which guarantees it has all the required properties and methods.
// Base interface
interface Animal {
name: string;
age: number;
sound(): string;
}
// Extended interface โ inherits Animal properties + adds more
interface Dog extends Animal {
breed: string;
isGoodBoy: boolean;
}
// Object matching the extended interface
const myDog: Dog = {
name: "Buddy",
age: 3,
breed: "Labrador",
isGoodBoy: true,
sound() {
return "Woof!";
},
};
console.log(`${myDog.name} the ${myDog.breed}`);
console.log(`Age: ${myDog.age}`);
console.log(`Sound: ${myDog.sound()}`);
console.log(`Good boy? ${myDog.isGoodBoy}`);
// Interface for a function
interface Formatter {
(value: string): string;
}
const shout: Formatter = (value) => value.toUpperCase();
const whisper: Formatter = (value) => value.toLowerCase();
console.log(shout("hello"));
console.log(whisper("GOODBYE"));Extending interfaces is how you avoid repeating property definitions. If you have Product, DigitalProduct, and PhysicalProduct, the common properties (id, name, price) go in Product and the specific ones in the extended interfaces. Changes to Product automatically apply everywhere.
Classes & OOP
TypeScript classes are like JavaScript classes but with types and access modifiers. Public properties and methods can be accessed from anywhere. Private ones can only be used inside the class. Protected ones can be used inside the class and its subclasses. TypeScript also has a constructor shorthand that declares and assigns properties in one go.
Classes with Access Modifiers
Access modifiers control who can see and use a property or method. public is the default โ anyone can access it. private means only the class itself can access it. TypeScript has a shorthand: putting public or private in the constructor parameters automatically creates and assigns the property.
class BankAccount {
// Shorthand: constructor params become properties
constructor(
public readonly id: string,
public owner: string,
private balance: number = 0,
) {}
// Public method โ anyone can call this
deposit(amount: number): void {
if (amount <= 0) {
console.log("Amount must be positive");
return;
}
this.balance += amount;
console.log(`Deposited R${amount}. Balance: R${this.balance}`);
}
withdraw(amount: number): void {
if (amount > this.balance) {
console.log("Insufficient funds");
return;
}
this.balance -= amount;
console.log(`Withdrew R${amount}. Balance: R${this.balance}`);
}
// Getter โ computed property that looks like a field
get formattedBalance(): string {
return `R${this.balance.toFixed(2)}`;
}
}
const account = new BankAccount("ACC001", "Alice");
console.log("Owner:", account.owner);
console.log("ID:", account.id);
account.deposit(1000);
account.deposit(500);
account.withdraw(200);
account.withdraw(2000); // should fail
console.log("Final balance:", account.formattedBalance);Making balance private prevents code outside the class from doing account.balance = 99999. The only way to change the balance is through the deposit and withdraw methods, which have validation. This is called encapsulation โ one of the key ideas of object-oriented programming.
Inheritance โ extends and super
A class can extend another class to inherit all its properties and methods. The child class can override methods to provide different behaviour. The super keyword calls the parent class constructor or method. Use implements to enforce that a class follows an interface.
// Base class
class Shape {
constructor(
public color: string = "black"
) {}
describe(): string {
return `A ${this.color} shape`;
}
area(): number {
return 0; // will be overridden
}
}
// Child class โ inherits from Shape
class Circle extends Shape {
constructor(
public radius: number,
color: string = "red",
) {
super(color); // call parent constructor
}
// Override the parent's method
area(): number {
return Math.PI * this.radius ** 2;
}
describe(): string {
return `${super.describe()} (circle, r=${this.radius})`;
}
}
class Rectangle extends Shape {
constructor(
public width: number,
public height: number,
color: string = "blue",
) {
super(color);
}
area(): number {
return this.width * this.height;
}
}
const c = new Circle(5);
const r = new Rectangle(4, 6);
console.log(c.describe());
console.log(`Circle area: ${c.area().toFixed(2)}`);
console.log(r.describe());
console.log(`Rectangle area: ${r.area()}`);You must call super() before accessing this in a child class constructor. This is enforced by both TypeScript and JavaScript. super() runs the parent's constructor first to set up the inherited properties, then your child constructor runs.
Generics
Generics let you write code that works with any type while still being type-safe. Instead of writing the same function for strings, numbers, and users, you write it once with a type parameter (usually T) that gets filled in at the call site. Generics are used everywhere in TypeScript โ in arrays, promises, API responses, and React components.
Generic Functions
Add a type parameter in angle brackets <T> after the function name. T is a placeholder that TypeScript will replace with the actual type when the function is called. You can use T as the type of parameters and the return value, which gives you type safety without losing flexibility.
// Without generics โ loses type information
function firstItemAny(arr: any[]): any {
return arr[0];
}
// With generics โ preserves type information
function firstItem<T>(arr: T[]): T | undefined {
return arr[0];
}
// TypeScript infers T from the argument
const firstNum = firstItem([10, 20, 30]);
const firstStr = firstItem(["apple", "banana", "cherry"]);
console.log("First number:", firstNum); // type: number
console.log("First string:", firstStr); // type: string
// Generic function with two type params
function pair<K, V>(key: K, value: V): [K, V] {
return [key, value];
}
const p1 = pair("name", "Alice");
const p2 = pair(1, true);
console.log("Pair 1:", p1);
console.log("Pair 2:", p2);
// Generic wrapper function
function identity<T>(value: T): T {
return value;
}
console.log(identity(42));
console.log(identity("hello"));
console.log(identity(true));T is just a naming convention โ you can call it anything. But the convention is T for the first type param, U for the second, K for key, and V for value. Using descriptive names like <TItem> or <TResponse> makes code more readable in complex cases.
Utility Types โ Partial, Pick, Omit, Readonly
TypeScript ships with built-in generic types called Utility Types that transform existing interfaces. Partial makes all properties optional. Readonly makes all properties immutable. Pick selects specific properties. Omit excludes specific properties. These save you from having to create duplicate interfaces.
interface Product {
id: number;
name: string;
price: number;
description: string;
inStock: boolean;
}
// Partial โ all properties become optional
// Useful for update operations (patch)
type ProductUpdate = Partial<Product>;
const update: ProductUpdate = {
price: 29.99, // Only updating price
};
console.log("Update payload:", update);
// Readonly โ nothing can be changed
type FrozenProduct = Readonly<Product>;
const item: FrozenProduct = {
id: 1, name: "Laptop", price: 9999,
description: "A great laptop", inStock: true,
};
// item.price = 0; // โ Error: Cannot assign to readonly property
// Pick โ select only certain properties
type ProductSummary = Pick<Product, "id" | "name" | "price">;
const summary: ProductSummary = { id: 1, name: "Laptop", price: 9999 };
console.log("Summary:", summary);
// Omit โ exclude certain properties
type ProductWithoutId = Omit<Product, "id">;
const newProduct: ProductWithoutId = {
name: "Mouse", price: 299,
description: "Wireless", inStock: true,
};
console.log("New product name:", newProduct.name);Partial<T> is one of the most used utility types โ especially for update/patch API endpoints where you only want to update some fields. Instead of making a whole new interface with everything optional, just wrap your existing one in Partial.
Advanced Types
Once you are comfortable with basics, TypeScript has more powerful features. Type aliases let you name complex types. Enums define a set of named constants. Type guards narrow down union types safely. Intersection types combine multiple types into one. These features are what make TypeScript genuinely powerful for large applications.
Type Aliases and Enums
A type alias gives a name to any type โ simple or complex. Enums define a named set of constants, which is much cleaner than using raw strings or numbers for things like status values, directions, or roles. Enums prevent typos and make your intent clear.
// Type alias โ name a type for reuse
type ID = string | number;
type Coordinates = { x: number; y: number };
type Callback = (error: string | null, data: string) => void;
const userId: ID = "user_123";
const point: Coordinates = { x: 10, y: 25 };
console.log("User ID:", userId);
console.log("Point:", point);
// Enum โ named set of constants
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}
enum Status {
Active = "ACTIVE",
Inactive = "INACTIVE",
Pending = "PENDING",
}
function move(direction: Direction): void {
console.log(`Moving: ${direction}`);
}
move(Direction.Up);
move(Direction.Left);
const userStatus: Status = Status.Active;
console.log("User status:", userStatus);
// Enum in a switch
function describeStatus(s: Status): string {
switch (s) {
case Status.Active: return "User is active";
case Status.Inactive: return "User is inactive";
case Status.Pending: return "Awaiting verification";
}
}
console.log(describeStatus(Status.Pending));Use string enums (Direction.Up = "UP") instead of numeric enums when possible. String enums are much easier to debug because the value is human-readable. Numeric enums just give you 0, 1, 2 which is meaningless when you log them.
Type Guards and Intersection Types
A type guard is a check that tells TypeScript which specific type you are working with inside a union. After the check, TypeScript narrows the type automatically. Intersection types combine multiple types into one using & โ the result must satisfy all of them at once.
// Type guard with typeof
function processValue(value: string | number): string {
if (typeof value === "string") {
// TypeScript knows value is string here
return "String: " + value.toUpperCase();
}
// TypeScript knows value is number here
return "Number: " + (value * 2).toString();
}
console.log(processValue("hello"));
console.log(processValue(21));
// Type guard with instanceof
class Cat {
meow() { return "Meow!"; }
}
class Fish {
swim() { return "Splash!"; }
}
function makeSound(pet: Cat | Fish): string {
if (pet instanceof Cat) {
return pet.meow();
}
return pet.swim();
}
console.log(makeSound(new Cat()));
console.log(makeSound(new Fish()));
// Intersection type โ combines multiple types
type Timestamped = { createdAt: Date };
type Named = { name: string };
type NamedRecord = Named & Timestamped;
const record: NamedRecord = {
name: "Alice",
createdAt: new Date("2025-01-01"),
};
console.log(record.name, record.createdAt.getFullYear());Type guards are the correct alternative to casting (value as string). Casting tells TypeScript "trust me, I know the type" and bypasses type checking. Type guards actually prove the type at runtime with real logic, which is much safer and catches more bugs.
You finished the TypeScript tutorial!
You can now write type-safe TypeScript with variables, functions, interfaces, classes, generics and advanced type utilities. These are the skills used daily at companies like Microsoft, Google, Airbnb and thousands more.