Skip to content

TypeScript for JavaScript developers: the parts that matter most

TypeScript catches whole categories of bugs before your code runs and makes large codebases far easier to change. Here’s a practical tour of the features you’ll use every day — and how to adopt it without slowing your team down.

Nasir Mollah

6 min read

On this page12 sections

JavaScript lets you move fast. It also lets you call a function with the wrong arguments, misspell a property, forget that a value might be undefined, and only find out when a user hits the bug in production.

TypeScript is JavaScript with a type system layered on top. You write almost the same code, add type information where it helps, and a compiler checks your whole project for mistakes before it runs. It has become the default for most serious frontend and Node.js projects — and for good reason.

What TypeScript is (and isn’t)

  • TypeScript is a superset of JavaScript: every valid JavaScript program is (almost always) valid TypeScript.
  • Types exist only at compile time. They’re removed when TypeScript is compiled to JavaScript. Browsers and Node.js run plain JavaScript.
  • Because types disappear at runtime, TypeScript can’t validate data coming from outside your program — API responses, form submissions, environment variables. You still need runtime validation for those (more on that below).

Why it’s worth it

  • Bugs caught early. Typos, wrong argument types, missing null checks and impossible states are flagged in your editor as you type.
  • Better editor support. Autocomplete, inline documentation, go-to-definition and safe automated renames work far better with types.
  • Confident refactoring. Rename a field or change a function signature and the compiler lists every place that needs updating.
  • Self-documenting code. Function signatures tell you what goes in and what comes out without reading the implementation.
  • Easier collaboration. Types are a contract between parts of a codebase — and between team members.

The basics

let title: string = "Booking confirmed";
let count: number = 3;
let isPaid: boolean = false;
let tags: string[] = ["new", "vip"];

function formatPrice(amount: number, currency: string): string {
  return new Intl.NumberFormat("en-US", { style: "currency", currency }).format(amount);
}

formatPrice("12", "USD"); // ❌ Error: Argument of type 'string' is not assignable to parameter of type 'number'.

Type inference: you write fewer types than you think

TypeScript infers types from values, so most variables don’t need annotations:

let title = "Booking confirmed"; // inferred as string
const total = items.reduce((sum, item) => sum + item.price, 0); // inferred as number

A good rule of thumb: annotate function parameters and public boundaries; let inference handle the rest.

Object types: interfaces and type aliases

interface User {
  id: string;
  name: string;
  email: string;
  phone?: string;            // optional
  readonly createdAt: Date;  // can't be reassigned
}

type Booking = {
  id: string;
  userId: User["id"];
  startsAt: Date;
  status: "pending" | "confirmed" | "cancelled";
};

interface and type overlap heavily. Many teams use interface for object shapes and type for unions and more complex compositions — pick a convention and stay consistent.

Union types and narrowing: TypeScript’s superpower

A union says a value can be one of several types:

type Status = "pending" | "confirmed" | "cancelled";
let status: Status = "confirmd"; // ❌ Error — typo caught

When a value could be several things, TypeScript makes you narrow it before use:

function greet(name: string | undefined) {
  return `Hello, ${name.toUpperCase()}`; // ❌ 'name' is possibly 'undefined'.
}

function greetSafely(name: string | undefined) {
  if (!name) return "Hello there";
  return `Hello, ${name.toUpperCase()}`; // ✅ name is a string here
}

This single check eliminates the most common JavaScript runtime error: “Cannot read properties of undefined”.

Discriminated unions: modeling states precisely

Combine unions with a shared “tag” field to model states where impossible combinations simply can’t be expressed:

type RequestState<T> =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: T }
  | { status: "error"; message: string };

function render(state: RequestState<User[]>) {
  switch (state.status) {
    case "idle":
      return "Search for a user";
    case "loading":
      return "Loading…";
    case "success":
      return `${state.data.length} users`;   // data only exists here
    case "error":
      return `Error: ${state.message}`;      // message only exists here
  }
}

There’s no way to have data and message at the same time, or to forget to handle a state without TypeScript noticing (with an exhaustiveness check). This pattern prevents an entire class of UI bugs.

Generics: reusable, type-safe code

Generics let functions and types work with many types while keeping type information:

function first<T>(items: T[]): T | undefined {
  return items[0];
}

const user = first(users);     // User | undefined
const price = first([10, 20]); // number | undefined

You’ll use generics constantly through libraries — useState<User | null>(null), Promise<Booking>, Map<string, number> — even if you rarely write your own.

Utility types worth knowing

TypeScript ships with helpers that transform existing types:

type UserPreview = Pick<User, "id" | "name">;  // only some fields
type UserUpdate = Partial<Omit<User, "id" | "createdAt">>; // all optional, minus some
type StatusLabels = Record<Booking["status"], string>; // an object with a key per status

const labels: StatusLabels = {
  pending: "Waiting for confirmation",
  confirmed: "Confirmed",
  cancelled: "Cancelled",
}; // ❌ if you forget a status, TypeScript tells you

Others you’ll meet often: Required<T>, Readonly<T>, ReturnType<typeof fn> and Awaited<T>.

The satisfies operator

satisfies checks that a value matches a type without widening it, so you keep precise inferred types:

const routes = {
  home: "/",
  work: "/work",
  contact: "/contact",
} satisfies Record<string, `/${string}`>;

routes.work; // type is "/work", not just string

Types stop at the boundary: validate runtime data

Because types disappear at runtime, this compiles but can still crash:

const res = await fetch("/api/user");
const user = (await res.json()) as User; // just a promise to the compiler, not a check

For data from outside — API responses, forms, process.env, webhooks (Server Actions included) — validate at runtime with a schema library such as Zod, and derive the TypeScript type from the schema so they never drift apart:

import { z } from "zod";

const UserSchema = z.object({
  id: z.string(),
  name: z.string().min(1),
  email: z.string().email(),
});

type User = z.infer<typeof UserSchema>;

const user = UserSchema.parse(await res.json()); // throws if the data is wrong

Configure strictness properly

TypeScript’s protection depends on its settings. In tsconfig.json, enable "strict": true for new projects. It turns on checks such as strictNullChecks (the undefined checks above) and noImplicitAny.

A few extra options worth considering:

  • noUncheckedIndexedAccess — treats array[i] and record[key] as possibly undefined, which they are.
  • noImplicitOverride, noFallthroughCasesInSwitch — catch subtle mistakes.

Habits that keep types useful

  • Avoid any. It switches type checking off and spreads silently. Use unknown for values you don’t know yet, then narrow them.
  • Don’t fight the compiler with as. Type assertions tell TypeScript “trust me”. Each one is a place where a bug can hide. Prefer narrowing or validation.
  • Avoid the non-null assertion ! except where you’re certain — it’s an as in disguise.
  • Model your domain. Use unions of literal strings instead of plain string for statuses, roles and modes.
  • Type your boundaries. Function signatures, API handlers and component props should be explicit.

Migrating an existing JavaScript project

You don’t need a big-bang rewrite:

  1. Add TypeScript and a tsconfig.json with "allowJs": true so JavaScript and TypeScript files can coexist.
  2. Optionally enable "checkJs": true to get type checking in .js files using inference and JSDoc comments.
  3. Convert files gradually, starting with shared utilities and data models — they give the biggest benefit to everything that uses them.
  4. Start lenient, then tighten: enable strict once most files are converted, fixing errors module by module.
  5. Add type checking to CI (tsc --noEmit) so new errors can’t creep in.

Does TypeScript slow you down?

There’s a small learning curve, and occasionally a complex library type will be confusing. But for anything bigger than a small script, most teams find they move faster overall: less time debugging, fewer regressions, safer refactors and much better autocomplete.

The takeaway

TypeScript doesn’t change what JavaScript can do — it changes how confidently you can do it. Start with strict mode, lean on inference, model states with unions, validate data at the boundaries, and avoid any. Your editor becomes a constant code reviewer, and a whole category of production bugs simply stops happening.

  • TypeScript
  • JavaScript
  • Web Development
  • Code Quality
  • 5 min read

    Web security essentials: the OWASP Top 10 explained for developers

    Most web breaches don’t need genius hackers — they exploit a familiar list of mistakes. Here’s a practical tour of the OWASP Top 10 risks, with concrete examples and the habits that prevent them.

    • Security
    • Web Development
    • Node.js
    • Code Quality
  • 5 min read

    Web accessibility for developers: a practical WCAG 2.2 guide

    Around one in six people lives with a significant disability — and accessible sites work better for everyone. Here’s what WCAG 2.2 asks for, the issues that matter most, and how to build and test accessible interfaces in everyday work.

    • Accessibility
    • Web Development
    • React
    • Code Quality

Building something? Let’s talk.

If you’re planning a web app, a website or a new feature, a free call is a good place to start — you’ll get honest options, a fixed price and a timeline.