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.
🔑 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 - No type checking function greet(name) { return "Hello, " + name; } greet(42); // No error, but probably a bug! greet(); // No error, returns "Hello, undefined"
// 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.
# 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
{
"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.
// 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.
// 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.
// 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.
// 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.
// 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 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.
// 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!
// "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.
// 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.
// 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.
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.
// 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.
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.
// 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.
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