TypeScript for JavaScript Developers

TypeScript catches the bugs JavaScript finds at runtime — in your editor, before you run anything. This guide covers the syntax that matters, interfaces and generics, strict mode, how to migrate a JS project gradually, and typing React components.

Written by Projectech7 min readPublished
For B.E./B.Tech Computer Science and IT students comfortable with JavaScript who keep hitting undefined-is-not-a-function bugs and want to understand what TypeScript actually buys them Topics: TypeScript, JavaScript, React, Frontend
Illustration of TypeScript adding type safety to JavaScript showing typed code with annotations catching errors before runtime.
Illustration generated for this guide.
In this guide

JavaScript lets you do anything — including passing a string where a number was expected, and finding out three function calls later when something explodes. You have felt this pain: cannot read property 'x' of undefined at runtime, in production, during the demo.

TypeScript is JavaScript with a type system: you describe the shapes of your data, and the compiler catches the mismatches before you run the code. This guide is the pragmatic introduction — what changes, what does not, and how to adopt it without drowning in type theory.

What TypeScript actually is (and is not)

  • It is a superset of JavaScript. Valid JS is valid TS. You can rename .js to .ts and add types gradually.
  • Types are erased at compile time. The browser runs plain JavaScript; TypeScript never affects runtime behavior or performance. It is purely a development-time safety net.
  • It is not a different language to relearn. If you know JavaScript, you know 90% of TypeScript. The new 10% is the type syntax.
npm install --save-dev typescript
npx tsc --init   # creates tsconfig.json

The core syntax in five minutes

// Basic annotations
let username: string = "ved";
let score: number = 95;
let isActive: boolean = true;

// Functions: annotate parameters AND return type
function calculateTotal(price: number, quantity: number): number {
  return price * quantity;
}

calculateTotal(100, 3);      // fine
calculateTotal("100", 3);    // compile error: string is not a number

That last line is the entire value proposition: the mistake is caught in your editor, underlined in red, before you ever run the code.

Interfaces: describing object shapes

interface User {
  id: number;
  name: string;
  email: string;
  role?: "admin" | "student";  // optional, limited to two values
}

function greet(user: User): string {
  return "Hello, " + user.name;
}

greet({ id: 1, name: "Asha", email: "asha@example.com" });  // fine
greet({ id: 1, name: "Asha" });  // error: email is missing

The ? marks optional properties; the "admin" | "student" is a union type — the value must be one of those literals. Misspell "amdin" and the compiler tells you immediately.

Type inference: you write less than you think

let count = 0;              // inferred as number, no annotation needed
const names = ["a", "b"];  // inferred as string[]

TypeScript infers obvious types. Annotate function boundaries and object shapes; let inference handle the rest. Beginners who annotate every variable write twice the code for no benefit.

The features that earn their keep

Union types and narrowing

function formatId(id: string | number): string {
  if (typeof id === "string") {
    return id.toUpperCase();   // TS knows id is string here
  }
  return id.toFixed(0);        // and number here
}

After a typeof check, TypeScript narrows the type inside each branch. This models real JavaScript (API responses that vary) without the usual guesswork.

Generics: reusable typed utilities

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

const n = first([1, 2, 3]);       // n: number | undefined
const s = first(["a", "b"]);     // s: string | undefined

One function, type-safe for any array type. You will meet generics constantly in React (useState<string>("")) and API clients — understanding this pattern unlocks reading library code.

Enums vs union literals

For a fixed set of string values, prefer union literals ("admin" | "student") over enums — they are simpler, work better with JSON APIs, and disappear at compile time. Reserve enums for numeric or complex cases.

Strict mode: turn it on

tsconfig.json has a strict flag. Turn it on from the start:

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true
  }
}

Strict mode enables strictNullChecks — the rule that string and string | undefined are different types. This single rule eliminates the largest category of runtime crashes: using a value that turned out to be null or undefined. It feels annoying for a week, then you cannot imagine working without it.

Migrating a JavaScript project pragmatically

  1. Rename and relax. Rename .js to .ts, set strict: false initially, and fix only the errors that appear. Your code compiles and runs the same.
  2. Type the boundaries first. API responses, function parameters, component props — the places where data enters your code. These give the most safety per annotation.
  3. Enable strict incrementally. Turn on strictNullChecks first (biggest win), fix the fallout, then full strict.
  4. Type third-party libraries via @types/ packages (e.g. @types/node, @types/express). Most popular libraries ship their own types now.
  5. Avoid any creep. any disables checking — useful as a temporary escape hatch during migration, but every any is a hole in the net. Prefer unknown (forces you to narrow before use) when you genuinely do not know the shape.

Note: any says "trust me, skip checking." unknown says "I don't know what this is — check before using." During migration, unknown plus narrowing is almost always the honest choice.

TypeScript with React (the common student case)

interface CardProps {
  title: string;
  subtitle?: string;
  highlighted?: boolean;
}

function renderCard({ title, subtitle, highlighted = false }: CardProps): string {
  const cls = highlighted ? "card card--highlighted" : "card";
  const sub = subtitle ? " - " + subtitle : "";
  return "[" + cls + "] " + title + sub;
}

Typed props turn "what does this component accept?" from a documentation hunt into something your editor answers instantly — autocomplete, inline errors, and refactoring that updates every usage safely.

Utility types you will use daily

TypeScript ships generic helpers that cover most type-manipulation needs — learn these five before writing clever custom types:

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

type UserUpdate = Partial<User>;        // all fields optional — for PATCH bodies
type UserPreview = Pick<User, "id" | "name">;  // subset — for list views
type UserNoRole = Omit<User, "role">;  // everything except — for public profiles
type IdMap = Record<number, User>;      // dictionary keyed by id
type Greeting = ReturnType<typeof greet>; // the return type of a function

Partial for update payloads, Pick/Omit for view-specific shapes, Record for lookup maps, ReturnType for deriving types from functions instead of duplicating them. These compose — Partial<Pick<User, "name" | "email">> is perfectly readable — and they keep your types derived from a single source of truth instead of redeclared in five places.

Common mistakes

  • Annotating everything. Let inference work. Annotate boundaries, not every let.
  • Reaching for any at the first error. The error is usually telling you something real about your code. Read it before silencing it.
  • Ignoring strict mode. Non-strict TypeScript is JavaScript with extra syntax — you pay the cost without getting the safety.
  • Over-engineering types. Deeply nested generics and conditional types in application code are a smell. Save the cleverness for libraries.
  • Typing implementation details. Type the public shape (props, API responses, function signatures); internal variables can stay inferred.
  • Forgetting types are compile-time only. TypeScript cannot validate data arriving from an API at runtime — for that you need runtime validation (e.g. Zod schemas), which pairs beautifully with TS types.

Quick checklist

  • TypeScript installed; tsc --noEmit (or your build) passes
  • strict: true in tsconfig
  • Interfaces for API shapes, props, and domain objects
  • No any in new code (unknown + narrowing instead)
  • Third-party types installed where needed
  • Editor showing inline type errors (VS Code does this out of the box)

Where to go from here

More project guides

More in Web Development