TypeScript — 0 to Hero
TypeScript is a statically typed superset of JavaScript developed by Microsoft. It adds optional type annotations, interfaces, generics, and other type-level constructs to JavaScript, catching errors at compile time rather than runtime. TypeScript compiles down to plain JavaScript — any valid JS is valid TS.
Created by Anders Hejlsberg (also behind C# and Delphi), TypeScript was first released in 2012. It has since become the dominant language for large-scale JavaScript applications, used by Angular, React, Vue, and virtually every major framework.
TypeScript's type system is structural, not nominal — types are compatible based on their shape, not their name. This makes it flexible while still catching real bugs. The type system is also Turing-complete, enabling advanced patterns like conditional types, mapped types, and template literal types.
JavaScript is dynamically typed — type errors surface at runtime, often in production. TypeScript adds a compile-time type checker that catches these errors before code ships. Beyond safety, TypeScript provides better IDE support (autocomplete, go-to-definition, refactoring), self-documenting code, and easier onboarding for large teams.
| 1 | // JavaScript — this "works" until runtime |
| 2 | function add(a, b) { |
| 3 | return a + b; |
| 4 | } |
| 5 | add(1, "2"); // "12" — silent string concatenation, probably a bug |
| 6 | |
| 7 | // TypeScript — this catches the error at compile time |
| 8 | function add(a: number, b: number): number { |
| 9 | return a + b; |
| 10 | } |
| 11 | add(1, "2"); // Error: Argument of type 'string' is not assignable to parameter of type 'number' |
note
Getting started with TypeScript requires the TypeScript compiler (tsc) and a configuration file. Most projects use npm or yarn to manage the dependency.
| 1 | # Install TypeScript globally or as a dev dependency |
| 2 | npm install -g typescript |
| 3 | |
| 4 | # Or per project (recommended) |
| 5 | npm init -y |
| 6 | npm install -D typescript |
| 7 | |
| 8 | # Verify installation |
| 9 | tsc --version # e.g., 5.4.5 |
| 10 | |
| 11 | # Compile a file |
| 12 | tsc hello.ts # outputs hello.js |
| 13 | |
| 14 | # Watch mode — recompile on changes |
| 15 | tsc --watch |
| 16 | |
| 17 | # Initialize tsconfig.json (recommended for all projects) |
| 18 | tsc --init |
| 1 | // hello.ts |
| 2 | function greet(name: string): string { |
| 3 | return `Hello, ${name}!`; |
| 4 | } |
| 5 | |
| 6 | const message = greet("TypeScript"); |
| 7 | console.log(message); // Hello, TypeScript! |
best practice
TypeScript's type system is one of the most powerful in any mainstream language. It supports structural typing, type inference, generics, conditional types, and more. Here are the core concepts:
| Category | Examples | Purpose |
|---|---|---|
| Primitives | string, number, boolean, null, undefined | Basic value types |
| Complex | array, tuple, object, enum | Compound data structures |
| Special | any, unknown, void, never, never | Control type checking behavior |
| Composed | union, intersection, type alias | Combine types |
| Structural | interface, type, class | Define shapes |
| 1 | // TypeScript infers types when possible |
| 2 | let x = 42; // inferred: number |
| 3 | let s = "hello"; // inferred: string |
| 4 | let arr = [1, 2, 3]; // inferred: number[] |
| 5 | |
| 6 | // Explicit annotations when needed |
| 7 | let name: string = "Alice"; |
| 8 | let age: number = 30; |
| 9 | let active: boolean = true; |
| 10 | |
| 11 | // Type inference is usually enough |
| 12 | function multiply(a: number, b: number) { |
| 13 | return a * b; // return type inferred as number |
| 14 | } |
TypeScript adds several features on top of JavaScript. Here are the most impactful ones:
| 1 | // Interfaces — define object shapes |
| 2 | interface User { |
| 3 | name: string; |
| 4 | age: number; |
| 5 | email?: string; // optional |
| 6 | } |
| 7 | |
| 8 | // Generics — reusable type-safe components |
| 9 | function first<T>(items: T[]): T | undefined { |
| 10 | return items[0]; |
| 11 | } |
| 12 | |
| 13 | const num = first([1, 2, 3]); // inferred: number | undefined |
| 14 | const str = first(["a", "b"]); // inferred: string | undefined |
| 15 | |
| 16 | // Type narrowing — TypeScript knows the type inside branches |
| 17 | function process(value: string | number) { |
| 18 | if (typeof value === "string") { |
| 19 | return value.toUpperCase(); // TS knows: string |
| 20 | } |
| 21 | return value.toFixed(2); // TS knows: number |
| 22 | } |
| 23 | |
| 24 | // Enums — named constants |
| 25 | enum Direction { |
| 26 | Up = "UP", |
| 27 | Down = "DOWN", |
| 28 | Left = "LEFT", |
| 29 | Right = "RIGHT", |
| 30 | } |
| 31 | |
| 32 | const move = Direction.Up; // "UP" |
info
The tsc compiler reads your TypeScript files and emits JavaScript. The tsconfig.json file controls compilation settings, including target ECMAScript version, module system, strictness, and file inclusion.
| 1 | { |
| 2 | "compilerOptions": { |
| 3 | "target": "ES2022", |
| 4 | "module": "ESNext", |
| 5 | "moduleResolution": "bundler", |
| 6 | "strict": true, |
| 7 | "esModuleInterop": true, |
| 8 | "skipLibCheck": true, |
| 9 | "outDir": "./dist", |
| 10 | "rootDir": "./src", |
| 11 | "declaration": true, |
| 12 | "sourceMap": true |
| 13 | }, |
| 14 | "include": ["src/**/*"], |
| 15 | "exclude": ["node_modules", "dist"] |
| 16 | } |
| Flag | What It Does |
|---|---|
| strict | Enables all strict type-checking options |
| target | ECMAScript version for output (ES5, ES2020, ES2022) |
| module | Module system (CommonJS, ESNext, NodeNext) |
| declaration | Emit .d.ts declaration files |
| sourceMap | Generate source maps for debugging |
| noUncheckedIndexedAccess | Array/object index access returns T | undefined |
best practice
This guide covers the essentials. From here, dive into the specific topics below to build a deep understanding of TypeScript's type system and features.
| 1 | // The journey ahead: |
| 2 | // 1. Types & Annotations — primitives, arrays, tuples, unions |
| 3 | // 2. Type Inference — how TS figures out types for you |
| 4 | // 3. Literals & Unions — literal types, discriminated unions |
| 5 | // 4. Enums — named constants and reverse mappings |
| 6 | // 5. Function Types — signatures, overloads, callbacks |
| 7 | // 6. Interfaces & Classes — OOP patterns in TypeScript |
| 8 | // 7. Generics — reusable type-safe components |
| 9 | // 8. Utility Types — Partial, Pick, Omit, Record, etc. |
| 10 | // 9. Advanced Types — conditional, mapped, template literal |
| 11 | // 10. tsconfig Deep Dive — compiler configuration mastery |
Beyond this 0-to-hero overview, ForgeLearn now ships a dual-audience mastery curriculum, a complete types encyclopedia, and focused deep dives for modern TypeScript.
| Page | What you get |
|---|---|
| How to Master TypeScript | Stages 0–7, checkpoints, agent curls, verification prompts |
| Complete Types Reference | Primitives, utilities, keyof/infer, mapped, satisfies |
| satisfies | Inference preservation vs as vs annotation |
| Narrowing | Control flow, guards, assertions, discriminated unions |
| Zod + TypeScript | Runtime validation aligned with z.infer |
| Strict Mode | Flag-by-flag migration JS→TS |
| React + TypeScript | Props, hooks, events, generic components |
info
TypeScript compares shapes, not names. Two types with the same properties are compatible even if declared separately. This is powerful — and surprising if you come from nominal languages like Java or C#.
| 1 | type Point2D = { x: number; y: number }; |
| 2 | type Vec2 = { x: number; y: number }; |
| 3 | const p: Point2D = { x: 1, y: 2 }; |
| 4 | const v: Vec2 = p; // OK — same shape |
| 5 | |
| 6 | class User { constructor(public name: string) {} } |
| 7 | class Person { constructor(public name: string) {} } |
| 8 | const u: User = new Person("Ada"); // OK at compile time |
warning
| 1 | type Options = { debug?: boolean }; |
| 2 | const o = { debug: true, verbose: true }; |
| 3 | const a: Options = { debug: true, verbose: true }; // Error — excess verbose |
| 4 | const b: Options = o; // OK — not a fresh literal |
A compact catalog of patterns that show up in real apps. Each links to a deeper page.
| 1 | // 1) Optional fields + defaults |
| 2 | type Cfg = { port?: number }; |
| 3 | function start({ port = 3000 }: Cfg = {}) {} |
| 4 | |
| 5 | // 2) Discriminated union state |
| 6 | type Remote<T> = |
| 7 | | { status: "idle" } |
| 8 | | { status: "loading" } |
| 9 | | { status: "success"; data: T } |
| 10 | | { status: "error"; error: Error }; |
| 11 | |
| 12 | // 3) keyof + indexed access |
| 13 | function pluck<T, K extends keyof T>(obj: T, key: K): T[K] { |
| 14 | return obj[key]; |
| 15 | } |
| 16 | |
| 17 | // 4) const assertion for literal unions |
| 18 | const MODES = ["dev", "prod"] as const; |
| 19 | type Mode = (typeof MODES)[number]; |
| 20 | |
| 21 | // 5) satisfies for configs |
| 22 | const theme = { primary: "#3178C6" } as const satisfies Record<string, `#${string}`>; |
Go deeper: narrowing, satisfies, generics, types reference.
| 1 | npm i -D typescript |
| 2 | npx tsc --init |
| 3 | # enable "strict": true in tsconfig.json |
| 4 | npx tsc --noEmit |
| 1 | { |
| 2 | "compilerOptions": { |
| 3 | "target": "ES2022", |
| 4 | "module": "NodeNext", |
| 5 | "moduleResolution": "NodeNext", |
| 6 | "strict": true, |
| 7 | "noUncheckedIndexedAccess": true, |
| 8 | "skipLibCheck": true, |
| 9 | "esModuleInterop": true, |
| 10 | "forceConsistentCasingInFileNames": true |
| 11 | }, |
| 12 | "include": ["src"] |
| 13 | } |
best practice
Valid JavaScript is valid TypeScript. Start by renaming .js → .ts, turning on allowJs, then ratcheting strictness. Add annotations where inference is insufficient — not everywhere.
| 1 | // JS habit |
| 2 | function add(a, b) { return a + b; } |
| 3 | |
| 4 | // TS — annotate params; return type often inferred |
| 5 | function addTs(a: number, b: number) { |
| 6 | return a + b; // number |
| 7 | } |
| 8 | |
| 9 | // Untrusted input |
| 10 | function parse(raw: unknown) { |
| 11 | if (typeof raw !== "string") throw new Error("string"); |
| 12 | return raw.toUpperCase(); |
| 13 | } |
Full migration playbook: /docs/typescript/strict. Runtime validation: /docs/typescript/zod.
Extra worked material to reinforce index. Keep strict: true enabled while experimenting.
| 1 | // Depth set 1 for index |
| 2 | type Id0 = string; |
| 3 | type Box0<T> = { value: T }; |
| 4 | function wrap0<T>(value: T): Box0<T> { |
| 5 | return { value }; |
| 6 | } |
| 7 | const sample0 = wrap0({ ok: true as const, n: 0 }); |
| 8 | type Sample0 = typeof sample0; |
| 9 | |
| 10 | type Keys0 = keyof Sample0; |
| 11 | type Val0 = Sample0["value"]; |
| 12 | |
| 13 | type Union0 = "a" | "b" | "c"; |
| 14 | type Upper0 = Uppercase<Union0>; |
| 15 | |
| 16 | type Fn0 = (x: number) => string; |
| 17 | type Ret0 = ReturnType<Fn0>; |
| 18 | type Params0 = Parameters<Fn0>; |
| 19 | |
| 20 | type PartialUser0 = Partial<{ id: string; name: string }>; |
| 21 | type RequiredUser0 = Required<PartialUser0>; |
| 22 |
note
| Check | Pass criteria |
|---|---|
| Inference | Hover types match expectations |
| Strict null | No implicit undefined holes |
| Refactor | Rename propagates safely |
Extra worked material to reinforce index. Keep strict: true enabled while experimenting.
| 1 | // Depth set 2 for index |
| 2 | type Id1 = string; |
| 3 | type Box1<T> = { value: T }; |
| 4 | function wrap1<T>(value: T): Box1<T> { |
| 5 | return { value }; |
| 6 | } |
| 7 | const sample1 = wrap1({ ok: true as const, n: 1 }); |
| 8 | type Sample1 = typeof sample1; |
| 9 | |
| 10 | type Keys1 = keyof Sample1; |
| 11 | type Val1 = Sample1["value"]; |
| 12 | |
| 13 | type Union1 = "a" | "b" | "c"; |
| 14 | type Upper1 = Uppercase<Union1>; |
| 15 | |
| 16 | type Fn1 = (x: number) => string; |
| 17 | type Ret1 = ReturnType<Fn1>; |
| 18 | type Params1 = Parameters<Fn1>; |
| 19 | |
| 20 | type PartialUser1 = Partial<{ id: string; name: string }>; |
| 21 | type RequiredUser1 = Required<PartialUser1>; |
| 22 |
note
| Check | Pass criteria |
|---|---|
| Inference | Hover types match expectations |
| Strict null | No implicit undefined holes |
| Refactor | Rename propagates safely |
Extra worked material to reinforce index. Keep strict: true enabled while experimenting.
| 1 | // Depth set 3 for index |
| 2 | type Id2 = string; |
| 3 | type Box2<T> = { value: T }; |
| 4 | function wrap2<T>(value: T): Box2<T> { |
| 5 | return { value }; |
| 6 | } |
| 7 | const sample2 = wrap2({ ok: true as const, n: 2 }); |
| 8 | type Sample2 = typeof sample2; |
| 9 | |
| 10 | type Keys2 = keyof Sample2; |
| 11 | type Val2 = Sample2["value"]; |
| 12 | |
| 13 | type Union2 = "a" | "b" | "c"; |
| 14 | type Upper2 = Uppercase<Union2>; |
| 15 | |
| 16 | type Fn2 = (x: number) => string; |
| 17 | type Ret2 = ReturnType<Fn2>; |
| 18 | type Params2 = Parameters<Fn2>; |
| 19 | |
| 20 | type PartialUser2 = Partial<{ id: string; name: string }>; |
| 21 | type RequiredUser2 = Required<PartialUser2>; |
| 22 |
note
| Check | Pass criteria |
|---|---|
| Inference | Hover types match expectations |
| Strict null | No implicit undefined holes |
| Refactor | Rename propagates safely |
Extra worked material to reinforce index. Keep strict: true enabled while experimenting.
| 1 | // Depth set 4 for index |
| 2 | type Id3 = string; |
| 3 | type Box3<T> = { value: T }; |
| 4 | function wrap3<T>(value: T): Box3<T> { |
| 5 | return { value }; |
| 6 | } |
| 7 | const sample3 = wrap3({ ok: true as const, n: 3 }); |
| 8 | type Sample3 = typeof sample3; |
| 9 | |
| 10 | type Keys3 = keyof Sample3; |
| 11 | type Val3 = Sample3["value"]; |
| 12 | |
| 13 | type Union3 = "a" | "b" | "c"; |
| 14 | type Upper3 = Uppercase<Union3>; |
| 15 | |
| 16 | type Fn3 = (x: number) => string; |
| 17 | type Ret3 = ReturnType<Fn3>; |
| 18 | type Params3 = Parameters<Fn3>; |
| 19 | |
| 20 | type PartialUser3 = Partial<{ id: string; name: string }>; |
| 21 | type RequiredUser3 = Required<PartialUser3>; |
| 22 |
note
| Check | Pass criteria |
|---|---|
| Inference | Hover types match expectations |
| Strict null | No implicit undefined holes |
| Refactor | Rename propagates safely |
Community
Get help on Slack, Discord or VIP
Stuck on a guide? Join the community and ask.