Toolmingo
Guides23 min read

TypeScript Tutorial for Beginners (2025): Learn TS Step by Step

Complete TypeScript tutorial for beginners. Learn types, interfaces, generics, classes, and how to migrate from JavaScript. Free guide with code examples and real projects.

TypeScript is JavaScript with types — and those types catch bugs before your code ever runs. This tutorial takes you from zero TypeScript knowledge to writing type-safe applications, with practical examples at every step.

What you'll learn

Topic What you'll be able to do
Setup Compile and run TypeScript files
Basic types Annotate variables, parameters, return values
Interfaces & types Define shapes for objects and data
Functions Write typed, overloaded functions
Classes Use OOP with full type safety
Generics Write reusable, flexible typed code
Utility types Use built-in TypeScript helpers
Modules Organise code into typed modules
Projects Build 3 real TypeScript programs

TypeScript version: 5.x (2025, all modern features)


Part 1 — Why TypeScript?

Feature JavaScript TypeScript
Type checking Runtime errors Compile-time errors
IDE support Basic autocomplete Full IntelliSense
Refactoring Manual, risky Safe, automated
Documentation Comments only Types are docs
Team collaboration Easy to break Contracts enforced
Learning curve None extra Small upfront cost

The core promise

// JavaScript — this crashes at runtime
function greet(user) {
  return "Hello, " + user.nme; // typo: nme instead of name
}
greet({ name: "Alice" }); // "Hello, undefined" — no error!
// TypeScript — this fails at compile time
function greet(user: { name: string }): string {
  return "Hello, " + user.nme; // TS error: Property 'nme' does not exist
}

TypeScript catches this instantly, before you even run the code.

TypeScript vs JavaScript vs other typed languages

Language Typing Runs where Best for
JavaScript Dynamic Browser + Node.js Web, quick scripts
TypeScript Static (optional) Browser + Node.js Large web apps, teams
Java Static, strict JVM Enterprise backend
C# Static, strict .NET Windows apps, game dev
Python Dynamic (type hints) Everywhere Data science, scripting
Go Static Everywhere System tools, APIs

TypeScript gives you JavaScript's ecosystem with the safety of a statically typed language.


Part 2 — Setup

Install TypeScript

# Install globally
npm install -g typescript

# Verify
tsc --version   # TypeScript 5.x.x

Your first TypeScript file

Create hello.ts:

const message: string = "Hello, TypeScript!";
console.log(message);

Compile and run:

tsc hello.ts      # produces hello.js
node hello.js     # Hello, TypeScript!

Project setup with tsconfig.json

For real projects, create a config file:

tsc --init

tsconfig.json essentials:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "commonjs",
    "strict": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules"]
}

Compile and watch

tsc          # compile once
tsc --watch  # recompile on file changes

Run TypeScript directly (development)

npm install -g ts-node
ts-node src/index.ts

Or with tsx (faster):

npm install -g tsx
tsx src/index.ts

Part 3 — Basic Types

Primitive types

// String
let name: string = "Alice";
let greeting: string = `Hello, ${name}!`;

// Number (all numbers: integer + float)
let age: number = 30;
let price: number = 9.99;
let hex: number = 0xff;

// Boolean
let isActive: boolean = true;
let hasAccount: boolean = false;

// BigInt
let bigNum: bigint = 9007199254740991n;

// Symbol
let id: symbol = Symbol("id");

Special types

// null and undefined
let nothing: null = null;
let missing: undefined = undefined;

// any — escape hatch (avoid when possible)
let anything: any = 42;
anything = "now a string"; // no error, no type checking

// unknown — safer than any
let userInput: unknown = getInput();
if (typeof userInput === "string") {
  console.log(userInput.toUpperCase()); // safe after narrowing
}

// never — a function that never returns
function throwError(message: string): never {
  throw new Error(message);
}

// void — function with no return value
function logMessage(msg: string): void {
  console.log(msg);
}

Type inference

TypeScript often infers types automatically:

let count = 0;           // inferred: number
let label = "error";     // inferred: string
let active = true;       // inferred: boolean

count = "hello";         // Error: Type 'string' is not assignable to type 'number'

Rule: Let TypeScript infer when obvious. Annotate when the type isn't clear.

Type annotation quick reference

Type Example Notes
string let s: string = "hi" Text
number let n: number = 42 All numbers
boolean let b: boolean = true true/false
null let n: null = null Explicit null
undefined let u: undefined Not yet set
any let a: any Opt-out of checking
unknown let u: unknown Safe any
never Return type Never returns
void Return type No return value
object let o: object Non-primitive

Part 4 — Arrays and Tuples

Arrays

// Two equivalent syntaxes
let numbers: number[] = [1, 2, 3];
let strings: Array<string> = ["a", "b", "c"];

// Array of objects
let users: { name: string; age: number }[] = [
  { name: "Alice", age: 30 },
  { name: "Bob", age: 25 },
];

// Readonly array — prevents mutation
const ids: readonly number[] = [1, 2, 3];
ids.push(4); // Error: Property 'push' does not exist on type 'readonly number[]'

Tuples

A tuple is a fixed-length array where each position has a specific type:

// Tuple: [string, number]
let person: [string, number] = ["Alice", 30];
let name = person[0]; // string
let age = person[1];  // number

// Labeled tuple (TypeScript 4.0+)
type Point = [x: number, y: number, z?: number];
const p: Point = [10, 20];       // z is optional
const p3d: Point = [10, 20, 30]; // all three

// Destructuring a tuple
const [firstName, userAge]: [string, number] = ["Bob", 25];

Part 5 — Interfaces and Type Aliases

These are the two main ways to define shapes in TypeScript.

Interface

interface User {
  id: number;
  name: string;
  email: string;
  age?: number;       // optional
  readonly role: string; // cannot be changed after creation
}

function getUser(id: number): User {
  return { id, name: "Alice", email: "alice@example.com", role: "admin" };
}

const user = getUser(1);
user.role = "user"; // Error: Cannot assign to 'role' because it is a read-only property

Type alias

type Point = {
  x: number;
  y: number;
};

type ID = string | number; // union type

type Status = "active" | "inactive" | "pending"; // literal union

Interface vs type alias

Feature Interface Type alias
Object shapes Yes Yes
Union types No Yes
Intersection (&) Via extends Yes
Extending extends keyword & operator
Declaration merging Yes (can reopen) No
Best for Objects, classes Unions, primitives, utilities
// Extending an interface
interface Animal {
  name: string;
}
interface Dog extends Animal {
  breed: string;
}

// Extending a type alias
type Animal2 = { name: string };
type Dog2 = Animal2 & { breed: string };

// Declaration merging (interface only)
interface Window {
  myCustomProp: string; // adds to the existing Window interface
}

Rule of thumb: Use interface for objects/classes. Use type for unions, intersections, and mapped types.


Part 6 — Union and Intersection Types

Union types

A value can be one of several types:

type ID = string | number;

function printId(id: ID): void {
  if (typeof id === "string") {
    console.log(id.toUpperCase()); // string methods available
  } else {
    console.log(id.toFixed(0));    // number methods available
  }
}

printId("abc123"); // works
printId(42);       // works

Discriminated unions

The pattern for modelling state machines:

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; side: number }
  | { kind: "rectangle"; width: number; height: number };

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

Intersection types

Combine multiple types into one:

type Timestamped = { createdAt: Date; updatedAt: Date };
type Identifiable = { id: string };

type Entity = Timestamped & Identifiable;

const post: Entity = {
  id: "post-1",
  createdAt: new Date(),
  updatedAt: new Date(),
};

Part 7 — Functions

Function type annotations

// Parameter and return type
function add(a: number, b: number): number {
  return a + b;
}

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

// Optional parameter
function greet(name: string, greeting?: string): string {
  return `${greeting ?? "Hello"}, ${name}!`;
}

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

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

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

Function types

// Function type alias
type MathOp = (a: number, b: number) => number;

const add: MathOp = (a, b) => a + b;
const sub: MathOp = (a, b) => a - b;

// Function as parameter
function applyOp(a: number, b: number, op: MathOp): number {
  return op(a, b);
}

applyOp(10, 3, add); // 13
applyOp(10, 3, sub); // 7

Overloads

Different signatures for different inputs:

function formatInput(input: string): string;
function formatInput(input: number): string;
function formatInput(input: string | number): string {
  if (typeof input === "string") {
    return input.trim().toLowerCase();
  }
  return input.toFixed(2);
}

formatInput("  Hello  ");  // "hello"
formatInput(3.14159);       // "3.14"

Part 8 — Classes

TypeScript adds access modifiers and other features to JavaScript classes.

Basic class

class Animal {
  // Class fields with types
  name: string;
  private age: number;    // only accessible inside this class
  protected species: string; // accessible in subclasses

  constructor(name: string, age: number, species: string) {
    this.name = name;
    this.age = age;
    this.species = species;
  }

  speak(): string {
    return `${this.name} makes a sound.`;
  }

  getAge(): number {
    return this.age; // accessible here
  }
}

const cat = new Animal("Whiskers", 3, "Felis catus");
cat.name;           // "Whiskers"
cat.age;            // Error: Property 'age' is private
cat.getAge();       // 3

Constructor shorthand

// Declare and assign in one step
class User {
  constructor(
    public name: string,
    private email: string,
    readonly id: number
  ) {}

  getEmail(): string {
    return this.email;
  }
}

const user = new User("Alice", "alice@example.com", 1);
user.name;    // "Alice"
user.email;   // Error: private
user.id;      // 1
user.id = 2;  // Error: readonly

Inheritance

class Dog extends Animal {
  breed: string;

  constructor(name: string, age: number, breed: string) {
    super(name, age, "Canis lupus familiaris");
    this.breed = breed;
  }

  speak(): string {
    return `${this.name} barks!`; // override parent method
  }

  describe(): string {
    // can access protected species from parent
    return `${this.name} is a ${this.breed} (${this.species})`;
  }
}

const rex = new Dog("Rex", 2, "Labrador");
rex.speak();    // "Rex barks!"
rex.describe(); // "Rex is a Labrador (Canis lupus familiaris)"

Abstract classes

abstract class Shape {
  abstract area(): number; // must be implemented by subclasses

  describe(): string {
    return `This shape has area: ${this.area().toFixed(2)}`;
  }
}

class Circle extends Shape {
  constructor(private radius: number) {
    super();
  }

  area(): number {
    return Math.PI * this.radius ** 2;
  }
}

const c = new Circle(5);
c.area();     // 78.54
c.describe(); // "This shape has area: 78.54"

Implementing interfaces

interface Serializable {
  serialize(): string;
  deserialize(data: string): void;
}

interface Loggable {
  log(): void;
}

class DataStore implements Serializable, Loggable {
  private data: Record<string, unknown> = {};

  serialize(): string {
    return JSON.stringify(this.data);
  }

  deserialize(data: string): void {
    this.data = JSON.parse(data);
  }

  log(): void {
    console.log(this.data);
  }
}

Access modifier summary

Modifier Class Subclass Outside
public (default) Yes Yes Yes
protected Yes Yes No
private Yes No No
readonly Read only Read only Read only

Part 9 — Generics

Generics let you write flexible, reusable code that works with any type.

Generic functions

// Without generics — loses type info
function identity(arg: any): any {
  return arg;
}

// With generics — type is preserved
function identity<T>(arg: T): T {
  return arg;
}

identity<string>("hello");   // type: string
identity<number>(42);        // type: number
identity("inferred");        // type inferred as string

Generic with constraints

// T must have a length property
function logLength<T extends { length: number }>(arg: T): T {
  console.log(`Length: ${arg.length}`);
  return arg;
}

logLength("hello");    // Length: 5
logLength([1, 2, 3]);  // Length: 3
logLength(42);         // Error: number doesn't have length

Generic interfaces and classes

interface Repository<T> {
  findById(id: number): T | undefined;
  findAll(): T[];
  save(item: T): void;
  delete(id: number): void;
}

class InMemoryRepo<T extends { id: number }> implements Repository<T> {
  private items: T[] = [];

  findById(id: number): T | undefined {
    return this.items.find(item => item.id === id);
  }

  findAll(): T[] {
    return [...this.items];
  }

  save(item: T): void {
    const index = this.items.findIndex(i => i.id === item.id);
    if (index >= 0) {
      this.items[index] = item;
    } else {
      this.items.push(item);
    }
  }

  delete(id: number): void {
    this.items = this.items.filter(i => i.id !== id);
  }
}

// Usage
interface Product { id: number; name: string; price: number }
const productRepo = new InMemoryRepo<Product>();
productRepo.save({ id: 1, name: "Widget", price: 9.99 });
productRepo.findById(1); // { id: 1, name: "Widget", price: 9.99 }

Multiple type parameters

function pair<A, B>(first: A, second: B): [A, B] {
  return [first, second];
}

const result = pair("age", 30); // [string, number]

Part 10 — Utility Types

TypeScript has built-in utility types for common transformations.

Most useful utility types

interface User {
  id: number;
  name: string;
  email: string;
  password: string;
  role: "admin" | "user";
}

// Partial — all properties optional
type PartialUser = Partial<User>;
// { id?: number; name?: string; email?: string; ... }

// Required — all properties required
type RequiredUser = Required<PartialUser>;
// same as User

// Readonly — all properties readonly
type ReadonlyUser = Readonly<User>;
// cannot modify any property

// Pick — select some properties
type PublicUser = Pick<User, "id" | "name" | "role">;
// { id: number; name: string; role: "admin" | "user" }

// Omit — remove some properties
type SafeUser = Omit<User, "password">;
// { id: number; name: string; email: string; role: "admin" | "user" }

// Record — dictionary type
type UserMap = Record<string, User>;
const users: UserMap = {
  "user-1": { id: 1, name: "Alice", email: "a@a.com", password: "hash", role: "user" },
};

// Exclude — remove from union
type AdminOrUser = "admin" | "user" | "guest";
type RegularRole = Exclude<AdminOrUser, "admin">; // "user" | "guest"

// Extract — keep matching types from union
type AdminOnly = Extract<AdminOrUser, "admin">; // "admin"

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

// ReturnType — extract function return type
function getUser(): User { /* ... */ return {} as User; }
type UserReturn = ReturnType<typeof getUser>; // User

// Parameters — extract function parameter types
type GetUserParams = Parameters<typeof getUser>; // []

// Awaited — unwrap Promise
type ApiResponse = Promise<{ data: User[] }>;
type Resolved = Awaited<ApiResponse>; // { data: User[] }

Utility types quick reference

Utility Description Example
Partial<T> All props optional Form state
Required<T> All props required Validated object
Readonly<T> No mutation Config objects
Pick<T, K> Select keys DTO / API response
Omit<T, K> Remove keys Hide sensitive fields
Record<K, V> Dictionary Lookup maps
Exclude<T, U> Remove from union Filtering roles
Extract<T, U> Keep matching Filtering roles
NonNullable<T> Remove null/undefined After validation
ReturnType<F> Function return type Infer from function
Parameters<F> Function params Proxy functions
Awaited<T> Unwrap Promise Async return types

Part 11 — Type Narrowing

TypeScript narrows types based on runtime checks.

type Input = string | number | null;

function process(input: Input): string {
  // typeof guard
  if (typeof input === "string") {
    return input.toUpperCase(); // string methods available
  }

  // null check
  if (input === null) {
    return "no input";
  }

  // After ruling out string and null, input must be number
  return input.toFixed(2);
}

// instanceof guard
function formatDate(value: string | Date): string {
  if (value instanceof Date) {
    return value.toISOString();
  }
  return new Date(value).toISOString();
}

// in operator — checks if property exists
type Cat = { meow: () => void };
type Dog = { bark: () => void };

function makeSound(animal: Cat | Dog): void {
  if ("meow" in animal) {
    animal.meow(); // Cat
  } else {
    animal.bark(); // Dog
  }
}

// Type predicates — custom type guards
function isString(value: unknown): value is string {
  return typeof value === "string";
}

function process2(value: unknown): void {
  if (isString(value)) {
    console.log(value.toUpperCase()); // TypeScript knows it's string
  }
}

Part 12 — Enums

// Numeric enum (default, starts at 0)
enum Direction {
  Up,    // 0
  Down,  // 1
  Left,  // 2
  Right, // 3
}

// String enum (more readable, preferred)
enum Status {
  Active = "ACTIVE",
  Inactive = "INACTIVE",
  Pending = "PENDING",
}

function processStatus(status: Status): string {
  switch (status) {
    case Status.Active: return "Processing...";
    case Status.Inactive: return "Skipped.";
    case Status.Pending: return "Waiting...";
  }
}

processStatus(Status.Active); // "Processing..."

// Const enum — inlined at compile time (no runtime object)
const enum Color {
  Red = "RED",
  Green = "GREEN",
  Blue = "BLUE",
}

// Alternative: literal union (often preferred over enums)
type Direction2 = "up" | "down" | "left" | "right";

Part 13 — Modules and Declaration Files

Module exports and imports

// math.ts
export function add(a: number, b: number): number {
  return a + b;
}

export const PI = 3.14159;

export interface Vector2D {
  x: number;
  y: number;
}

// Default export
export default class Calculator {
  add(a: number, b: number) { return a + b; }
}

// main.ts
import Calculator, { add, PI, Vector2D } from "./math";

const calc = new Calculator();
const sum = add(3, 4);
const point: Vector2D = { x: 1, y: 2 };

Type-only imports

// Import only the type, not the value — better for tree-shaking
import type { Vector2D } from "./math";

type Points = Vector2D[];

Declaration files (.d.ts)

When using a JavaScript library without types:

# Most popular libraries have types in DefinitelyTyped
npm install --save-dev @types/lodash
npm install --save-dev @types/node

If no types exist, create a declaration file:

// types/my-library.d.ts
declare module "my-library" {
  export function doSomething(input: string): number;
  export const version: string;
}

Part 14 — Advanced Types

Mapped types

// Make all properties optional
type Optional<T> = {
  [K in keyof T]?: T[K];
};

// Make all properties nullable
type Nullable<T> = {
  [K in keyof T]: T[K] | null;
};

// Extract read-only version
type Mutable<T> = {
  -readonly [K in keyof T]: T[K]; // remove readonly
};

Template literal types

type EventName = "click" | "focus" | "blur";
type EventHandler = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur"

type CSSValue = `${number}px` | `${number}em` | `${number}rem`;
const margin: CSSValue = "16px"; // valid
const bad: CSSValue = "16";      // Error

Conditional types

type IsArray<T> = T extends any[] ? true : false;

type A = IsArray<string[]>; // true
type B = IsArray<string>;   // false

// Infer inside conditional types
type UnpackArray<T> = T extends (infer Item)[] ? Item : T;

type C = UnpackArray<string[]>; // string
type D = UnpackArray<number>;   // number

Part 15 — Three Projects

Project 1: Type-safe Todo CLI

// todo-cli.ts
interface Todo {
  id: number;
  text: string;
  done: boolean;
  createdAt: Date;
}

type CreateTodoInput = Omit<Todo, "id" | "createdAt">;

class TodoList {
  private todos: Todo[] = [];
  private nextId = 1;

  add(input: CreateTodoInput): Todo {
    const todo: Todo = {
      id: this.nextId++,
      text: input.text,
      done: input.done,
      createdAt: new Date(),
    };
    this.todos.push(todo);
    return todo;
  }

  complete(id: number): boolean {
    const todo = this.todos.find(t => t.id === id);
    if (!todo) return false;
    todo.done = true;
    return true;
  }

  list(filter?: "all" | "done" | "pending"): Todo[] {
    switch (filter) {
      case "done": return this.todos.filter(t => t.done);
      case "pending": return this.todos.filter(t => !t.done);
      default: return [...this.todos];
    }
  }

  remove(id: number): boolean {
    const before = this.todos.length;
    this.todos = this.todos.filter(t => t.id !== id);
    return this.todos.length < before;
  }
}

// Usage
const list = new TodoList();
list.add({ text: "Learn TypeScript", done: false });
list.add({ text: "Build a project", done: false });
list.complete(1);
console.log(list.list("done")); // [ { id: 1, text: "Learn TypeScript", ... } ]

Project 2: Type-safe REST API client

// api-client.ts
interface ApiConfig {
  baseUrl: string;
  defaultHeaders?: Record<string, string>;
}

interface ApiResponse<T> {
  data: T | null;
  error: string | null;
  status: number;
}

interface Post {
  id: number;
  title: string;
  body: string;
  userId: number;
}

class ApiClient {
  constructor(private config: ApiConfig) {}

  private async request<T>(
    method: "GET" | "POST" | "PUT" | "DELETE",
    path: string,
    body?: unknown
  ): Promise<ApiResponse<T>> {
    const url = `${this.config.baseUrl}${path}`;
    const headers: Record<string, string> = {
      "Content-Type": "application/json",
      ...this.config.defaultHeaders,
    };

    try {
      const response = await fetch(url, {
        method,
        headers,
        body: body ? JSON.stringify(body) : undefined,
      });

      if (!response.ok) {
        return {
          data: null,
          error: `HTTP ${response.status}: ${response.statusText}`,
          status: response.status,
        };
      }

      const data: T = await response.json();
      return { data, error: null, status: response.status };
    } catch (err) {
      return {
        data: null,
        error: err instanceof Error ? err.message : "Unknown error",
        status: 0,
      };
    }
  }

  get<T>(path: string): Promise<ApiResponse<T>> {
    return this.request<T>("GET", path);
  }

  post<T>(path: string, body: unknown): Promise<ApiResponse<T>> {
    return this.request<T>("POST", path, body);
  }

  put<T>(path: string, body: unknown): Promise<ApiResponse<T>> {
    return this.request<T>("PUT", path, body);
  }

  delete<T>(path: string): Promise<ApiResponse<T>> {
    return this.request<T>("DELETE", path);
  }
}

// Usage
const client = new ApiClient({
  baseUrl: "https://jsonplaceholder.typicode.com",
});

async function main(): Promise<void> {
  const { data, error } = await client.get<Post[]>("/posts");

  if (error) {
    console.error("Failed:", error);
    return;
  }

  if (data) {
    data.slice(0, 3).forEach(post => {
      console.log(`${post.id}: ${post.title}`);
    });
  }
}

main();

Project 3: Generic event emitter

// event-emitter.ts
type EventMap = Record<string, unknown[]>;

type EventListener<Args extends unknown[]> = (...args: Args) => void;

class TypedEventEmitter<Events extends EventMap> {
  private listeners: {
    [K in keyof Events]?: EventListener<Events[K]>[];
  } = {};

  on<K extends keyof Events>(
    event: K,
    listener: EventListener<Events[K]>
  ): this {
    if (!this.listeners[event]) {
      this.listeners[event] = [];
    }
    this.listeners[event]!.push(listener);
    return this;
  }

  off<K extends keyof Events>(
    event: K,
    listener: EventListener<Events[K]>
  ): this {
    const list = this.listeners[event];
    if (list) {
      this.listeners[event] = list.filter(l => l !== listener) as typeof list;
    }
    return this;
  }

  emit<K extends keyof Events>(event: K, ...args: Events[K]): void {
    this.listeners[event]?.forEach(listener => listener(...args));
  }

  once<K extends keyof Events>(
    event: K,
    listener: EventListener<Events[K]>
  ): this {
    const wrapper: EventListener<Events[K]> = (...args) => {
      listener(...args);
      this.off(event, wrapper);
    };
    return this.on(event, wrapper);
  }
}

// Define your event types
type AppEvents = {
  login: [userId: string, timestamp: Date];
  logout: [userId: string];
  error: [message: string, code: number];
};

// Usage — fully type-safe
const emitter = new TypedEventEmitter<AppEvents>();

emitter.on("login", (userId, timestamp) => {
  // userId: string, timestamp: Date — fully typed!
  console.log(`${userId} logged in at ${timestamp.toISOString()}`);
});

emitter.on("error", (message, code) => {
  // message: string, code: number — fully typed!
  console.error(`Error ${code}: ${message}`);
});

emitter.emit("login", "user-123", new Date());
emitter.emit("error", "Not found", 404);
// emitter.emit("login", 123, new Date()); // Error: number not assignable to string

Migrating from JavaScript to TypeScript

Step-by-step approach

  1. Rename .js files to .ts — fix errors one by one
  2. Add tsconfig.json with "strict": false initially
  3. Enable strict mode gradually — turn on one flag at a time
  4. Install type packagesnpm install -D @types/node @types/react etc.
  5. Replace any with real types progressively

Common migration patterns

// JS: implicit any
function processData(data) {
  return data.map(item => item.value);
}

// TS: explicit types
function processData(data: Array<{ value: number }>): number[] {
  return data.map(item => item.value);
}

// JS: object without shape
const config = {
  host: "localhost",
  port: 3000,
};

// TS: typed config
interface ServerConfig {
  host: string;
  port: number;
  ssl?: boolean;
}

const config: ServerConfig = {
  host: "localhost",
  port: 3000,
};

TypeScript with Popular Frameworks

Framework How to start Notes
React npm create vite -- --template react-ts TSX + typed props
Next.js npx create-next-app --typescript Full-stack TS
Vue 3 npm create vue (select TS) <script setup lang="ts">
Express npm i express @types/express Typed middleware
NestJS nest new project TypeScript-first framework
Prisma npm install prisma Auto-generates types from schema

React + TypeScript quick example

// Button.tsx
interface ButtonProps {
  label: string;
  onClick: () => void;
  variant?: "primary" | "secondary" | "danger";
  disabled?: boolean;
}

export function Button({
  label,
  onClick,
  variant = "primary",
  disabled = false,
}: ButtonProps): JSX.Element {
  return (
    <button
      onClick={onClick}
      disabled={disabled}
      className={`btn btn-${variant}`}
    >
      {label}
    </button>
  );
}

// Usage — TypeScript enforces all prop types
<Button
  label="Submit"
  onClick={() => console.log("clicked")}
  variant="primary"
/>

<Button label="Delete" onClick={handleDelete} variant="danger" />

Learning Path

Stage Duration What to focus on
1. Basics 1 week Types, interfaces, functions, classes
2. Intermediate 2 weeks Generics, utility types, narrowing
3. Advanced 2 weeks Mapped types, conditional types, decorators
4. Framework 2–4 weeks React/Next.js or Node.js/Express with TS
5. Real project Ongoing Build something, read open-source TS code

Common Mistakes

Mistake Problem Fix
Using any everywhere Defeats the purpose of TypeScript Use unknown + narrowing
Type assertions (as) too early Bypasses checking Add proper type guard instead
Ignoring compiler errors Bugs reach production Fix errors, don't use // @ts-ignore
interface for everything Some cases need type Use type for unions
enum overuse Larger bundle, runtime object Use const enum or literal unions
Not enabling strict mode Misses many checks Set "strict": true in tsconfig
Typing everything manually Verbose, error-prone Let TypeScript infer where it can
Ignoring @types/ packages Runtime crashes with JS libs Always install @types/ for JS libraries

TypeScript vs Related Terms

Term What it is
TypeScript JavaScript superset with static types
JavaScript Dynamic language TypeScript compiles to
TSC TypeScript compiler (tsc CLI)
tsconfig.json TypeScript project config file
.d.ts file Type declaration file (no runtime code)
DefinitelyTyped Community @types/ packages repository
ESLint + typescript-eslint Linting for TypeScript code
Babel JS transpiler (can strip TS types, no checking)
SWC Fast Rust-based TS/JS transpiler
Zod Runtime schema validation that matches TS types

Frequently Asked Questions

Do I need to know JavaScript before learning TypeScript?

Yes. TypeScript is a superset of JavaScript — every JS concept (closures, promises, prototype chain) still applies. Learn JS fundamentals first, then TypeScript becomes a natural addition.

Is TypeScript required for React/Node.js?

No, but it's strongly recommended. Most new React and Node.js projects in 2025 use TypeScript. Job listings increasingly list TS as a requirement.

Does TypeScript slow down development?

Initially, yes. Setting up types takes extra time upfront. Long-term, TypeScript speeds development through better autocomplete, safer refactoring, and fewer runtime bugs.

What's strict mode in TypeScript?

Enabling "strict": true in tsconfig turns on several strict checks: noImplicitAny, strictNullChecks, strictFunctionTypes, and others. Always enable it for new projects.

What's the difference between TypeScript and JavaScript at runtime?

There is no TypeScript at runtime — the compiler strips all types and produces plain JavaScript. TypeScript only exists at development and compile time.

Should I use interface or type?

Use interface for defining object shapes that might be extended or implemented by classes. Use type for unions, intersections, mapped types, and utility transformations. When in doubt, either works.


Quick Reference

// Variables
let name: string = "Alice";
let count: number = 0;
let active: boolean = true;
let anything: unknown = getValue();

// Arrays
let nums: number[] = [1, 2, 3];
let strs: Array<string> = ["a", "b"];

// Tuple
let pair: [string, number] = ["age", 30];

// Interface
interface User { id: number; name: string; age?: number }

// Type alias
type Status = "active" | "inactive";

// Generic function
function wrap<T>(value: T): T[] { return [value]; }

// Utility types
type P = Partial<User>;   // all optional
type S = Pick<User, "id">; // select keys
type O = Omit<User, "age">; // remove keys

// Union
function handle(input: string | number): void { /* ... */ }

// Intersection
type Admin = User & { permissions: string[] };

// Type narrowing
if (typeof value === "string") { /* value is string */ }
if (value instanceof Date) { /* value is Date */ }

// Optional chaining + nullish coalescing
const city = user?.address?.city ?? "Unknown";

// Non-null assertion (use sparingly)
const el = document.getElementById("app")!;

// Type assertion (use sparingly)
const input = event.target as HTMLInputElement;

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools