Back to Insights
Tutorials

Introduction to TypeScript for JavaScript Developers

Ahmed Raza Dec 18, 2025 10 min read

Why TypeScript?

TypeScript adds static typing to JavaScript, catching errors before runtime and improving code quality, maintainability, and developer experience.

Benefits of TypeScript

Catch Errors Early:

// JavaScript - Error at runtime
function greet(name) {
  return name.toUppercase(); // Typo!
}

// TypeScript - Error at compile time
function greet(name: string): string {
  return name.toUppercase(); // Error: Property 'toUppercase' does not exist
}

Better IDE Support:

  • Autocomplete
  • Inline documentation
  • Refactoring tools
  • Go to definition
  • Find all references

Self-Documenting Code:

// Clear what this function expects and returns
function calculateTax(amount: number, rate: number): number {
  return amount * rate;
}

Easier Refactoring:

  • Rename variables safely
  • Change interfaces confidently
  • Find all usages instantly

Setting Up TypeScript

Install TypeScript:

npm install -g typescript

Initialize Project:

mkdir my-ts-project
cd my-ts-project
npm init -y
npm install typescript --save-dev
tsc --init

tsconfig.json:

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

Compile TypeScript:

tsc            # Compile once
tsc --watch    # Watch mode

Basic Types

Primitive Types:

// String
let name: string = "John";

// Number
let age: number = 30;

// Boolean
let isActive: boolean = true;

// Null and Undefined
let nothing: null = null;
let notDefined: undefined = undefined;

// Any (avoid when possible)
let anything: any = "could be anything";

Arrays:

// Array of numbers
let numbers: number[] = [1, 2, 3];
let moreNumbers: Array<number> = [4, 5, 6];

// Array of strings
let names: string[] = ["John", "Jane"];

// Array of mixed types
let mixed: (number | string)[] = [1, "two", 3];

Tuples:

// Fixed-length array with specific types
let person: [string, number] = ["John", 30];

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

Enums:

// Numeric enum
enum Direction {
  Up, // 0
  Down, // 1
  Left, // 2
  Right, // 3
}

// String enum
enum Status {
  Active = "ACTIVE",
  Inactive = "INACTIVE",
  Pending = "PENDING",
}

let currentStatus: Status = Status.Active;

Interfaces and Types

Interfaces:

interface User {
  id: number;
  name: string;
  email: string;
  age?: number; // Optional property
  readonly createdAt: Date; // Read-only property
}

const user: User = {
  id: 1,
  name: "John Doe",
  email: "john@example.com",
  createdAt: new Date(),
};

// user.createdAt = new Date(); // Error: Cannot assign to readonly

Type Aliases:

type ID = number | string;
type Point = {
  x: number;
  y: number;
};

let userId: ID = "abc123";
let coordinate: Point = { x: 10, y: 20 };

Extending Interfaces:

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

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

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

Functions

Typing Functions:

// Function declaration
function add(a: number, b: number): number {
  return a + b;
}

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

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

// 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);
}

Function Types:

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

const add: MathOperation = (a, b) => a + b;
const subtract: MathOperation = (a, b) => a - b;

Void and Never:

// Void: function returns nothing
function logMessage(message: string): void {
  console.log(message);
}

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

Union and Intersection Types

Union Types:

// Can be one type OR another
type StringOrNumber = string | number;

function format(value: StringOrNumber): string {
  if (typeof value === "string") {
    return value.toUpperCase();
  }
  return value.toFixed(2);
}

// Union with literal types
type Status = "success" | "error" | "pending";

function handleStatus(status: Status) {
  // status can only be one of these three values
}

Intersection Types:

// Must have properties from ALL types
type Person = {
  name: string;
  age: number;
};

type Employee = {
  employeeId: string;
  department: string;
};

type StaffMember = Person & Employee;

const staff: StaffMember = {
  name: "John",
  age: 30,
  employeeId: "E123",
  department: "IT",
};

Generics

Generic Functions:

// Generic function
function identity<T>(value: T): T {
  return value;
}

let num = identity<number>(42);
let str = identity<string>("hello");

// Type inference
let inferred = identity("world"); // T is inferred as string

Generic Interfaces:

interface Box<T> {
  value: T;
}

let numberBox: Box<number> = { value: 42 };
let stringBox: Box<string> = { value: "hello" };

Generic Classes:

class DataStore<T> {
  private data: T[] = [];

  add(item: T): void {
    this.data.push(item);
  }

  get(index: number): T | undefined {
    return this.data[index];
  }

  getAll(): T[] {
    return this.data;
  }
}

const numberStore = new DataStore<number>();
numberStore.add(1);
numberStore.add(2);

const stringStore = new DataStore<string>();
stringStore.add("hello");
stringStore.add("world");

Classes

Basic Class:

class Person {
  // Properties
  name: string;
  private age: number;
  protected email: string;

  // Constructor
  constructor(name: string, age: number, email: string) {
    this.name = name;
    this.age = age;
    this.email = email;
  }

  // Method
  greet(): string {
    return `Hello, I'm ${this.name}`;
  }

  // Getter
  get info(): string {
    return `${this.name} (${this.age})`;
  }

  // Setter
  set updateAge(age: number) {
    if (age > 0) {
      this.age = age;
    }
  }
}

Shorthand Constructor:

class User {
  constructor(
    public name: string,
    private email: string,
    protected age: number,
  ) {}
}

Inheritance:

class Animal {
  constructor(public name: string) {}

  move(distance: number): void {
    console.log(`${this.name} moved ${distance}m`);
  }
}

class Dog extends Animal {
  constructor(
    name: string,
    public breed: string,
  ) {
    super(name);
  }

  bark(): void {
    console.log("Woof! Woof!");
  }
}

const dog = new Dog("Buddy", "Golden Retriever");
dog.move(10);
dog.bark();

Abstract Classes:

abstract class Shape {
  abstract area(): number;
  abstract perimeter(): number;

  describe(): string {
    return `Area: ${this.area()}, Perimeter: ${this.perimeter()}`;
  }
}

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

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

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

Type Guards

typeof:

function process(value: string | number) {
  if (typeof value === "string") {
    return value.toUpperCase();
  }
  return value.toFixed(2);
}

instanceof:

class Dog {
  bark() {
    console.log("Woof!");
  }
}

class Cat {
  meow() {
    console.log("Meow!");
  }
}

function makeSound(animal: Dog | Cat) {
  if (animal instanceof Dog) {
    animal.bark();
  } else {
    animal.meow();
  }
}

Custom Type Guards:

interface Fish {
  swim(): void;
}

interface Bird {
  fly(): void;
}

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

function move(pet: Fish | Bird) {
  if (isFish(pet)) {
    pet.swim();
  } else {
    pet.fly();
  }
}

Utility Types

Partial:

interface User {
  id: number;
  name: string;
  email: string;
}

// All properties optional
type PartialUser = Partial<User>;

function updateUser(user: User, updates: Partial<User>): User {
  return { ...user, ...updates };
}

Required:

interface Config {
  host?: string;
  port?: number;
}

// All properties required
type RequiredConfig = Required<Config>;

Pick:

type UserPreview = Pick<User, "id" | "name">;

Omit:

type UserWithoutEmail = Omit<User, "email">;

Record:

type PageInfo = {
  title: string;
  url: string;
};

type Pages = Record<"home" | "about" | "contact", PageInfo>;

Migrating JavaScript to TypeScript

Step 1: Rename Files

  • .js.ts
  • .jsx.tsx

Step 2: Add tsconfig.json Start with loose settings:

{
  "compilerOptions": {
    "strict": false,
    "noImplicitAny": false
  }
}

Step 3: Gradually Add Types Start with function parameters and return types

Step 4: Enable Strict Mode Once most code is typed:

{
  "compilerOptions": {
    "strict": true
  }
}

Best Practices

  1. Enable Strict Mode

    "strict": true
  2. Avoid any Use unknown when type is truly unknown

  3. Use Interfaces for Objects

    interface User { ... }
  4. Leverage Type Inference

    // Good: TypeScript infers number
    let count = 5;
    
    // Unnecessary: explicit type
    let count: number = 5;
  5. Use Union Types Over Enums

    type Status = "active" | "inactive" | "pending";
  6. Make Illegal States Unrepresentable

    // Bad
    interface User {
      loading: boolean;
      error: string | null;
      data: UserData | null;
    }
    
    // Good
    type User =
      | { status: "loading" }
      | { status: "error"; error: string }
      | { status: "success"; data: UserData };

Conclusion

TypeScript improves JavaScript development with type safety, better tooling, and self-documenting code. Start small, gradually add types, and embrace the compiler as your friend. The initial investment in learning TypeScript pays dividends in reduced bugs and improved maintainability.

Share This Insight

Related Insights

View All