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
.jsto.tsand 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
- Rename and relax. Rename
.jsto.ts, setstrict: falseinitially, and fix only the errors that appear. Your code compiles and runs the same. - Type the boundaries first. API responses, function parameters, component props — the places where data enters your code. These give the most safety per annotation.
- Enable strict incrementally. Turn on
strictNullChecksfirst (biggest win), fix the fallout, then fullstrict. - Type third-party libraries via
@types/packages (e.g.@types/node,@types/express). Most popular libraries ship their own types now. - Avoid
anycreep.anydisables checking — useful as a temporary escape hatch during migration, but everyanyis a hole in the net. Preferunknown(forces you to narrow before use) when you genuinely do not know the shape.
Note:
anysays "trust me, skip checking."unknownsays "I don't know what this is — check before using." During migration,unknownplus 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
anyat 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: truein tsconfig - Interfaces for API shapes, props, and domain objects
- No
anyin 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
- Framework context: Next.js vs React: which to learn and Next.js API Routes Explained.
- Test your typed code: Testing JavaScript with Jest.
- Compare backend stacks: React vs Django vs Laravel.
- Style the frontend: Tailwind CSS for beginners.
- More frontend topics in the Web Development branch hub.