TypeScript

TypeScript Tutorial

Add static typing to JavaScript for better developer experience and fewer bugs

1 What is TypeScript?

TypeScript is a strongly-typed superset of JavaScript developed by Microsoft. It adds optional static typing, classes, and interfaces to help catch errors at compile time.

TypeScript = JavaScript + Types
JavaScript
.js files
+
Types
Static typing
=
TypeScript
.ts files

🔑 Why TypeScript?

Catch Errors Early: Find bugs at compile time, not runtime
Better IDE Support: Autocomplete, refactoring, inline documentation
Self-Documenting: Types serve as documentation
Safer Refactoring: Compiler catches breaking changes

JavaScript vs TypeScript

JavaScript
// JavaScript - No type checking
function greet(name) {
    return "Hello, " + name;
}

greet(42);  // No error, but probably a bug!
greet();    // No error, returns "Hello, undefined"
TypeScript
// TypeScript - Type checking catches errors
function greet(name: string): string {
    return "Hello, " + name;
}

greet(42);  // ❌ Error: Argument of type 'number' is not assignable
greet();    // ❌ Error: Expected 1 argument, but got 0
greet("Alice");  // ✅ Works correctly

2 Setup & Configuration

TypeScript needs to be compiled to JavaScript before running. Here's how to set it up.

Terminal
# Install TypeScript globally
npm install -g typescript

# Check version
tsc --version

# Initialize TypeScript config
tsc --init

# Compile a file
tsc myfile.ts

# Watch mode (auto-compile on save)
tsc --watch
tsconfig.json
{
    "compilerOptions": {
        "target": "ES2020",          // Output JS version
        "module": "ESNext",          // Module system
        "strict": true,              // Enable all strict checks
        "outDir": "./dist",          // Output directory
        "rootDir": "./src",          // Source directory
        "esModuleInterop": true,     // ES module compatibility
        "skipLibCheck": true,        // Skip .d.ts checking
        "forceConsistentCasingInFileNames": true
    },
    "include": ["src/**/*"],
    "exclude": ["node_modules"]
}

💡 Quick Start with Vite

npm create vite@latest my-app -- --template react-ts
This creates a React + TypeScript project with everything configured!

3 Basic Types

TypeScript includes all JavaScript primitive types plus additional types for better type safety.

TypeScript - Basic Types
// PRIMITIVE TYPES
let name: string = "Alice";
let age: number = 25;
let isActive: boolean = true;

// Type inference (TypeScript figures it out)
let city = "NYC";        // Inferred as string
let count = 42;          // Inferred as number

// ANY - opt out of type checking (avoid!)
let data: any = 5;
data = "hello";          // No error, but defeats the purpose

// UNKNOWN - safer than any
let input: unknown = "test";
// input.toUpperCase();    // ❌ Error! Must check type first
if (typeof input === "string") {
    input.toUpperCase();   // ✅ Now TypeScript knows it's a string
}

// VOID - function returns nothing
function logMessage(msg: string): void {
    console.log(msg);
}

// NULL and UNDEFINED
let nothing: null = null;
let notDefined: undefined = undefined;

// NEVER - function never returns (throws or infinite loop)
function throwError(message: string): never {
    throw new Error(message);
}

⚠️ Avoid any

Using any disables type checking for that variable. It defeats the purpose of TypeScript! Use unknown if you truly don't know the type, or be more specific.

4 Arrays & Tuples

TypeScript arrays are typed - all elements must be of the specified type. Tuples are fixed-length arrays with specific types at each position.

TypeScript - Arrays
// ARRAYS - two syntax options
let numbers: number[] = [1, 2, 3, 4, 5];
let names: Array<string> = ["Alice", "Bob"];

// Type inference works for arrays too
let colors = ["red", "green", "blue"];  // string[]

// Array operations are type-safe
numbers.push(6);        // ✅ OK
// numbers.push("seven"); // ❌ Error: string not assignable to number

// Mixed arrays
let mixed: (string | number)[] = [1, "two", 3];

// TUPLES - fixed length, specific types
let person: [string, number] = ["Alice", 25];
// person = [25, "Alice"];  // ❌ Error: wrong order
// person = ["Alice"];      // ❌ Error: missing element

// Accessing tuple elements
let personName = person[0];  // type: string
let personAge = person[1];   // type: number

// Named tuples (better readability)
type Point = [x: number, y: number];
let point: Point = [10, 20];

// Optional tuple elements
type RGB = [number, number, number, number?];  // Alpha is optional
let red: RGB = [255, 0, 0];
let transparentRed: RGB = [255, 0, 0, 0.5];

// Readonly arrays
const frozen: readonly number[] = [1, 2, 3];
// frozen.push(4);  // ❌ Error: push doesn't exist on readonly

5 Objects

Object types define the shape of an object - what properties it has and their types.

TypeScript - Objects
// Inline object type
let user: { name: string; age: number } = {
    name: "Alice",
    age: 25
};

// Optional properties with ?
let config: { 
    theme: string; 
    fontSize?: number;  // Optional
} = {
    theme: "dark"
    // fontSize is optional, not required
};

// Readonly properties
let point: { 
    readonly x: number; 
    readonly y: number; 
} = { x: 10, y: 20 };
// point.x = 5;  // ❌ Error: Cannot assign to 'x'

// Index signatures (dynamic keys)
let scores: { [key: string]: number } = {
    alice: 95,
    bob: 87,
    charlie: 92
};
scores["david"] = 88;  // ✅ OK

// Nested objects
let company: {
    name: string;
    address: {
        street: string;
        city: string;
    };
} = {
    name: "TechCorp",
    address: {
        street: "123 Main St",
        city: "Boston"
    }
};

6 Interfaces

Interfaces are reusable type definitions for objects. They're the preferred way to define object shapes.

TypeScript - Interfaces
// Define an interface
interface User {
    id: number;
    name: string;
    email: string;
    age?: number;           // Optional
    readonly createdAt: Date;  // Cannot be changed
}

// Use the interface
const alice: User = {
    id: 1,
    name: "Alice",
    email: "alice@example.com",
    createdAt: new Date()
};

// Interface with methods
interface Calculator {
    add(a: number, b: number): number;
    subtract(a: number, b: number): number;
}

const calc: Calculator = {
    add(a, b) { return a + b; },
    subtract(a, b) { return a - b; }
};

// Extending interfaces
interface Animal {
    name: string;
}

interface Dog extends Animal {
    breed: string;
    bark(): void;
}

const buddy: Dog = {
    name: "Buddy",
    breed: "Golden Retriever",
    bark() { console.log("Woof!"); }
};

// Multiple inheritance
interface Flyable {
    fly(): void;
}

interface Bird extends Animal, Flyable {
    wingspan: number;
}

💡 Interface vs Type Alias

Interface: Best for object shapes, can be extended/merged
Type Alias: More flexible, works with unions, primitives
Rule of thumb: Use interfaces for objects, types for everything else

7 Type Aliases

Type aliases create new names for types. They're more flexible than interfaces and can represent any type.

TypeScript - Type Aliases
// Simple type alias
type ID = string | number;
type Status = "pending" | "approved" | "rejected";

let orderId: ID = "abc123";
let orderStatus: Status = "pending";

// Object type alias
type Point = {
    x: number;
    y: number;
};

type Point3D = Point & { z: number };  // Extend with intersection

// Function type alias
type GreetFunction = (name: string) => string;

const greet: GreetFunction = (name) => {
    return `Hello, ${name}!`;
};

// Callback type
type Callback<T> = (data: T) => void;

function fetchData(callback: Callback<string>) {
    callback("data");
}

// Literal types
type Direction = "north" | "south" | "east" | "west";
type DiceRoll = 1 | 2 | 3 | 4 | 5 | 6;

let direction: Direction = "north";
// direction = "up";  // ❌ Error: "up" not in Direction

8 Union & Intersection Types

Union types (|) allow a value to be one of several types. Intersection types (&) combine multiple types into one.

Union vs Intersection
A | B
Either A OR B
vs
A & B
Both A AND B
TypeScript - Union & Intersection
// UNION TYPES ( | )
type StringOrNumber = string | number;

function printId(id: StringOrNumber) {
    console.log("ID: " + id);
}

printId(101);      // ✅
printId("abc");   // ✅
// printId(true);  // ❌ boolean not in union

// Discriminated unions (common pattern)
interface Circle {
    kind: "circle";
    radius: number;
}

interface Rectangle {
    kind: "rectangle";
    width: number;
    height: number;
}

type Shape = Circle | Rectangle;

function getArea(shape: Shape): number {
    switch (shape.kind) {
        case "circle":
            return Math.PI * shape.radius ** 2;
        case "rectangle":
            return shape.width * shape.height;
    }
}

// INTERSECTION TYPES ( & )
interface HasName {
    name: string;
}

interface HasAge {
    age: number;
}

type Person = HasName & HasAge;  // Must have both

const person: Person = {
    name: "Alice",
    age: 25
    // Both required!
};

9 Type Narrowing

Type narrowing is the process of refining types to more specific types within conditional blocks.

TypeScript - Narrowing
// typeof narrowing
function padLeft(value: string, padding: string | number) {
    if (typeof padding === "number") {
        // TypeScript knows padding is number here
        return " ".repeat(padding) + value;
    }
    // TypeScript knows padding is string here
    return padding + value;
}

// Truthiness narrowing
function printName(name: string | null) {
    if (name) {
        // name is string (not null)
        console.log(name.toUpperCase());
    }
}

// instanceof narrowing
function logDate(x: Date | string) {
    if (x instanceof Date) {
        console.log(x.toISOString());
    } else {
        console.log(x.toUpperCase());
    }
}

// "in" operator narrowing
interface Fish { swim: () => void; }
interface Bird { fly: () => void; }

function move(animal: Fish | Bird) {
    if ("swim" in animal) {
        animal.swim();  // Fish
    } else {
        animal.fly();   // Bird
    }
}

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

10 Type Assertions

Type assertions tell TypeScript "trust me, I know what this type is." Use sparingly!

TypeScript - Assertions
// "as" syntax (preferred)
const input = document.getElementById("myInput") as HTMLInputElement;
input.value = "Hello";  // Now TypeScript knows it's an input

// Angle bracket syntax (doesn't work in JSX)
const input2 = <HTMLInputElement>document.getElementById("myInput");

// Non-null assertion (! operator)
function processValue(value: string | null) {
    // We're sure it's not null here
    console.log(value!.toUpperCase());  // ! tells TS "trust me"
}

// const assertion (makes values readonly)
const config = {
    endpoint: "/api",
    timeout: 3000
} as const;
// config.timeout = 5000;  // ❌ Error: readonly

// Array const assertion
const colors = ["red", "green", "blue"] as const;
// type: readonly ["red", "green", "blue"]

⚠️ Use Assertions Carefully

Type assertions bypass TypeScript's safety checks. They should be a last resort! Prefer type narrowing with if checks or type guards.

11 Functions

TypeScript adds type annotations for function parameters and return types.

TypeScript - Functions
// Basic function with types
function add(a: number, b: number): number {
    return a + b;
}

// Arrow function
const multiply = (a: number, b: number): number => a * b;

// Optional parameters
function greet(name: string, greeting?: string): string {
    return (greeting || "Hello") + ", " + name;
}

greet("Alice");           // "Hello, Alice"
greet("Bob", "Hi");       // "Hi, Bob"

// Default parameters
function createUser(name: string, role: string = "user") {
    return { name, role };
}

// Rest parameters
function sum(...numbers: number[]): number {
    return numbers.reduce((a, b) => a + b, 0);
}

sum(1, 2, 3, 4);  // 10

// Function overloads
function reverse(value: string): string;
function reverse(value: number[]): number[];
function reverse(value: string | number[]): string | number[] {
    if (typeof value === "string") {
        return value.split("").reverse().join("");
    }
    return value.reverse();
}

reverse("hello");     // "olleh" (string)
reverse([1, 2, 3]);   // [3, 2, 1] (number[])

12 Generics

Generics create reusable components that work with multiple types while maintaining type safety.

Generics = Type Parameters
identity<T>
Works with any type
identity<string>
Type-safe strings
TypeScript - Generics
// Generic function
function identity<T>(value: T): T {
    return value;
}

identity<string>("hello");  // type: string
identity<number>(42);        // type: number
identity("inferred");        // type inference works!

// Generic with arrays
function firstElement<T>(arr: T[]): T | undefined {
    return arr[0];
}

firstElement([1, 2, 3]);      // type: number
firstElement(["a", "b"]);    // type: string

// Multiple type parameters
function pair<K, V>(key: K, value: V): [K, V] {
    return [key, value];
}

pair("name", "Alice");   // [string, string]
pair(1, true);            // [number, boolean]

// Generic constraints
interface HasLength {
    length: number;
}

function logLength<T extends HasLength>(value: T): void {
    console.log(value.length);
}

logLength("hello");     // ✅ string has length
logLength([1, 2, 3]);   // ✅ array has length
// logLength(42);        // ❌ number has no length

// Generic interface
interface Box<T> {
    value: T;
    getValue(): T;
}

const stringBox: Box<string> = {
    value: "hello",
    getValue() { return this.value; }
};

13 Classes

TypeScript enhances JavaScript classes with access modifiers, abstract classes, and interface implementation.

TypeScript - Classes
class Person {
    // Access modifiers
    public name: string;        // accessible anywhere
    private age: number;        // only in this class
    protected email: string;   // this class + subclasses
    readonly id: number;       // cannot be changed
    
    constructor(name: string, age: number, email: string) {
        this.name = name;
        this.age = age;
        this.email = email;
        this.id = Math.random();
    }
    
    // Shorthand constructor (same as above)
    // constructor(public name: string, private age: number) {}
    
    getAge(): number {
        return this.age;
    }
}

// Inheritance
class Employee extends Person {
    constructor(
        name: string,
        age: number,
        email: string,
        public role: string
    ) {
        super(name, age, email);
    }
    
    getEmail(): string {
        return this.email;  // Can access protected
    }
}

// Implementing interfaces
interface Printable {
    print(): void;
}

class Document implements Printable {
    constructor(public content: string) {}
    
    print(): void {
        console.log(this.content);
    }
}

// Abstract class
abstract class Shape {
    abstract getArea(): number;  // Must implement
    
    describe(): void {
        console.log(`Area: ${this.getArea()}`);
    }
}

class Circle extends Shape {
    constructor(public radius: number) { super(); }
    
    getArea(): number {
        return Math.PI * this.radius ** 2;
    }
}

14 Enums

Enums define a set of named constants, making code more readable and type-safe.

TypeScript - Enums
// Numeric enum (default)
enum Direction {
    Up,       // 0
    Down,     // 1
    Left,     // 2
    Right     // 3
}

let dir: Direction = Direction.Up;
console.log(dir);  // 0

// Custom starting value
enum StatusCode {
    Success = 200,
    NotFound = 404,
    Error = 500
}

// String enum (recommended)
enum Color {
    Red = "RED",
    Green = "GREEN",
    Blue = "BLUE"
}

let color: Color = Color.Red;
console.log(color);  // "RED"

// Using enums
function getStatusMessage(code: StatusCode): string {
    switch (code) {
        case StatusCode.Success:
            return "Request successful";
        case StatusCode.NotFound:
            return "Resource not found";
        case StatusCode.Error:
            return "Server error";
    }
}

// const enum (inlined, no runtime cost)
const enum Size {
    Small = "sm",
    Medium = "md",
    Large = "lg"
}

💡 String Enums vs Union Types

For simple string constants, union types are often simpler:
type Status = "pending" | "approved" | "rejected";
Use enums when you need reverse mapping or grouping.

15 Utility Types

TypeScript includes built-in utility types for common type transformations.

TypeScript - Utility Types
interface User {
    id: number;
    name: string;
    email: string;
    age: number;
}

// Partial<T> - all properties optional
type PartialUser = Partial<User>;
const update: PartialUser = { name: "Alice" };  // OK!

// Required<T> - all properties required
type RequiredUser = Required<PartialUser>;

// Readonly<T> - all properties readonly
type ReadonlyUser = Readonly<User>;

// Pick<T, K> - select specific properties
type UserPreview = Pick<User, "id" | "name">;
// { id: number; name: string }

// Omit<T, K> - exclude specific properties
type UserWithoutEmail = Omit<User, "email">;
// { id: number; name: string; age: number }

// Record<K, T> - create object type with keys K and values T
type UserRoles = Record<string, User>;
const users: UserRoles = {
    admin: { id: 1, name: "Admin", email: "...", age: 30 }
};

// Exclude<T, U> - remove types from union
type Status = "pending" | "approved" | "rejected";
type ActiveStatus = Exclude<Status, "rejected">;
// "pending" | "approved"

// Extract<T, U> - extract types from union
type StringTypes = Extract<string | number | boolean, string>;
// string

// NonNullable<T> - remove null and undefined
type MaybeString = string | null | undefined;
type DefiniteString = NonNullable<MaybeString>;
// string

// ReturnType<T> - get function return type
function getUser() {
    return { id: 1, name: "Alice" };
}
type UserReturn = ReturnType<typeof getUser>;
// { id: number; name: string }

16 Mapped Types

Mapped types transform existing types by iterating over their properties.

TypeScript - Mapped Types
// Basic mapped type
type Nullable<T> = {
    [K in keyof T]: T[K] | null;
};

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

type NullableUser = Nullable<User>;
// { name: string | null; age: number | null }

// Making all properties optional (like Partial)
type MyPartial<T> = {
    [K in keyof T]?: T[K];
};

// Making all properties readonly
type MyReadonly<T> = {
    readonly [K in keyof T]: T[K];
};

// Remove readonly modifier
type Mutable<T> = {
    -readonly [K in keyof T]: T[K];
};

// Getters type
type Getters<T> = {
    [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

type UserGetters = Getters<User>;
// { getName: () => string; getAge: () => number }

// Template literal types
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<"click">;  // "onClick"

17 React + TypeScript

TypeScript works beautifully with React, providing type safety for props, state, and events.

React + TypeScript
import { useState, useEffect } from 'react';

// Props interface
interface ButtonProps {
    text: string;
    onClick: () => void;
    disabled?: boolean;
    variant?: "primary" | "secondary";
}

// Typed functional component
const Button: React.FC<ButtonProps> = ({ 
    text, 
    onClick, 
    disabled = false,
    variant = "primary"
}) => {
    return (
        <button
            onClick={onClick}
            disabled={disabled}
            className={`btn btn-${variant}`}
        >
            {text}
        </button>
    );
};

// useState with types
interface User {
    id: number;
    name: string;
}

function UserProfile() {
    // Explicit type
    const [user, setUser] = useState<User | null>(null);
    const [count, setCount] = useState(0);  // Inferred: number
    
    return user && <div>{user.name}</div>;
}

// Event handlers
function Form() {
    const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
        console.log(e.target.value);
    };
    
    const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
        e.preventDefault();
    };
    
    return (
        <form onSubmit={handleSubmit}>
            <input onChange={handleChange} />
        </form>
    );
}

// Children prop
interface CardProps {
    title: string;
    children: React.ReactNode;
}

const Card = ({ title, children }: CardProps) => (
    <div className="card">
        <h2>{title}</h2>
        {children}
    </div>
);

// Custom hook with types
function useFetch<T>(url: string): {
    data: T | null;
    loading: boolean;
    error: string | null;
} {
    const [data, setData] = useState<T | null>(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState<string | null>(null);
    
    useEffect(() => {
        fetch(url)
            .then(res => res.json())
            .then(setData)
            .catch(e => setError(e.message))
            .finally(() => setLoading(false));
    }, [url]);
    
    return { data, loading, error };
}

💡 Common React Types

React.FC<Props> - Functional component
React.ReactNode - Any renderable content
React.ChangeEvent<HTMLInputElement> - Input change
React.FormEvent<HTMLFormElement> - Form submit
React.MouseEvent<HTMLButtonElement> - Button click