πŸ“š Sanjay's Study Notes Home Roadmap HTML CSS JavaScript TypeScript Node SQL & Prisma React Tailwind Auth Apps Git & Deploy

04 Β· TypeScript β€” JavaScript that checks your work

You already know JavaScript β€” TypeScript IS JavaScript, plus a layer that reads your code before it runs and points at mistakes. Every JS line you've written is valid TS. You're not learning a new language; you're hiring a proofreader.

Where the name comes from JavaScript + Types = TypeScript. A type is the "kind" of a value β€” you've known this since Β§4 of the JS notes (string / number / boolean). TypeScript's whole job is making the types you already think about visible and enforced. Built by Microsoft in 2012 because large JS codebases were drowning in exactly the bugs you've been hitting.

1. Why TypeScript exists β€” your own bugs are the proof

Every one of these is a real bug you wrote in the Library app. JavaScript ran them silently. TypeScript refuses to compile them:

Your actual bugWhat JS didWhat TS says instead
books.lenght (typo)silent undefined, NaN laterProperty 'lenght' does not exist. Did you mean 'length'?
Math.max(books.map(...)) β€” no spreadsilent NaN idArgument of type 'number[]' is not assignable to parameter of type 'number'
books[books.find(...)] β€” object as indexsilent undefinedType 'Book' cannot be used as an index type
"5" + 3 stored where a number belongssilent "53"Type 'string' is not assignable to type 'number'
book.publisher (key that doesn't exist)silent undefinedProperty 'publisher' does not exist on type 'Book'
The big idea: compile time vs run time JS finds bugs at run time β€” when a user clicks the button. TS finds them at compile time β€” the moment you type them, as red squiggles in VS Code. The bug never reaches the browser. Notice the pattern in the table: JavaScript's favorite move is silent undefined/NaN that explodes three steps later, far from the real cause. TS kills the bug at its birthplace.
your code app.ts TYPE CHECK typos Β· wrong types missing keys Β· null passed βœ“ plain JavaScript app.js β†’ browser failed β†’ red squiggle, fix it NOW, browser never sees it
The wall: TypeScript checks first, and only clean code becomes the JavaScript the browser runs.
Why browsers don't run TS directly Browsers only speak JavaScript. So TS must be compiled (translated) to JS first β€” the compiler is called tsc (TypeScript Compiler). Big bonus: the check happens during translation, and the output is ordinary JS with the type notes stripped out. Types exist only at compile time β€” zero cost in the browser.

2. Setup β€” the compiler

WHERE do these commands go? The terminal β€” never the browser tsc is a program on your computer (like Node), not something the page runs. The browser never sees TypeScript at all. In VS Code: Terminal β†’ New Terminal (shortcut Ctrl+`) and type the commands there. The flow is: you write app.ts β†’ run tsc in the terminal β†’ it produces app.js β†’ the browser runs app.js. The browser stays as JS-only as ever; tsc does its checking before the browser is ever involved.
# one-time install (you have Node, so you have npm):
npm install -g typescript

# check it worked:
tsc --version

# compile one file: app.ts β†’ app.js
tsc app.ts

# create tsconfig.json (project settings) once:
tsc --init

# with a tsconfig, just:  tsc        (compiles the project)
# or better:              tsc --watch   (recompiles on every save)
What does -g mean? Global vs local installs -g = global: npm installs TypeScript once for your whole machine (on Windows it lands in C:\Users\<you>\AppData\Roaming\npm), not into the folder you're standing in. That's exactly why the tsc command then works from any terminal, in any folder. Which commands care about your current folder? npm install -g β€” doesn't care. tsc --init β€” cares a lot: it creates tsconfig.json in the folder you're standing in, so run it inside the project (LibraryManagementSystem).
Your machine, verified 2026-08-26 βœ… tsc --version β†’ 7.0.2 (the new fast native compiler) Β· tsc --init run inside LibraryManagementSystem/ Β· tsconfig.json generated with "strict": true.
tsconfig.json β€” the three settings that matter now tsc --init generates a big file; almost everything can stay default. Know these:
You edit .ts β€” the browser loads .js After converting, your <script> tag must point at the compiled .js file. The .ts file never goes in the HTML. Classic first-day confusion: "I fixed the .ts but the page didn't change" β€” did you recompile? (--watch exists to end that pain.)
YOU HIT THIS (2026-08-27): exports is not defined crash App compiled with 0 errors, but the browser console exploded: Uncaught ReferenceError: exports is not defined, then a cascade of Cannot access 'books' before initialization. The cause was tsconfig, not your code. The generated tsconfig had "module": "nodenext" + "moduleDetection": "force" β€” which told tsc "treat every file as a Node.js module" β€” so the emitted app.js contained Node-style plumbing (exports) that browsers don't have. Lesson: 0 compile errors β‰  runs in the browser. The compiler settings decide what KIND of JS comes out, and it has to match where that JS will run.

3. Type annotations β€” telling it what belongs in the box

An annotation is a colon + type after a name: "this box holds only this kind."

let title: string = "Harry Potter";
let copies: number = 5;
let isAvailable: boolean = true;

copies = "five";   // ❌ Type 'string' is not assignable to type 'number'

Arrays: the item type + []:

let titles: string[] = ["Harry Potter", "The Alchemist"];
let ids: number[] = [1, 2, 3, 4];

ids.push("5");   // ❌ string can't enter a number[] β€” the "53" bug, blocked at the door
Type inference β€” TS is not a paperwork machine Write let copies = 5; and TS infers number from the value β€” same protection, no annotation. Hover any variable in VS Code to see its inferred type. Rule: annotate where TS can't guess β€” function parameters, empty arrays (let loans: Loan[] = []) β€” and let inference do the rest. Beginner code drowning in unnecessary annotations is a smell.

4. Typing functions β€” contracts on the door

Parameters get annotations; the return type goes after the ( ):

function lateFine(daysLate: number): number {
    return daysLate * 2;
}

lateFine(5);      // 10
lateFine("5");    // ❌ caught β€” remember .value is always a string!
lateFine();       // ❌ Expected 1 argument, but got 0
Why this is the highest-value spot for types A function is a doorway β€” data flows through it from far away (a button, another function). In JS, garbage walks straight in and explodes somewhere deep inside (your NaN id traveled: form β†’ addBook β†’ push β†’ render β†’ screen). Typed parameters check ID at the door, so the blast happens at the call site where the mistake actually is. Arrow functions type the same way: (b: Book) => b.copies <= 2.
void β€” "returns nothing" Your renderBooks, saveBooks, borrowBook return nothing β€” their return type is void: function saveBooks(): void { ... }. TS usually infers it; writing it is documentation.

5. interface β€” the shape of a Book, written down

Your whole app runs on objects shaped like a book β€” but in JS that shape lived only in your head. An interface writes it down, and TS enforces it everywhere:

interface Book {
    id: number;
    title: string;
    author: string;
    category: string;
    copies: number;
}

let books: Book[] = [
    { id: 1, title: "Harry Potter", author: "J.K. Rowling", category: "Fantasy", copies: 5 }
];

books.push({ id: 5, title: "New Book" });
// ❌ missing: author, category, copies β€” no more half-built books EVER entering the array

book.titel;     // ❌ typo caught β€” plus autocomplete now knows every field
interface Book (the stencil) id: number title: string author: string category: string copies: number { id: 1, title: "HP", author: "JKR", ... all 5 } βœ“ fits the stencil { id: 5, title: "New" } copies? author? ❌ rejected
An interface is a stencil: every object claiming to be a Book must match it β€” exactly.
Why "interface"? In engineering, an interface is the agreed meeting surface between two parts β€” plug and socket. Code on both sides of the agreement (whoever builds books, whoever uses books) can now be checked against the same written contract. This is the single most-used TS keyword in real codebases β€” API responses, database rows, component props: everything gets an interface.
Optional fields: ? publisher?: string means "may or may not exist." Then TS forces you to handle the missing case before using it β€” Β§11's "missing key = undefined" surprise, now supervised.

6. Union types & undefined β€” your find is about to argue

A union means "one of these types," written with | (read it as "or"):

let memberId: number | null = null;      // "a number, or nothing yet"
let category: "Fantasy" | "Fiction" | "Autobiography";   // only these exact strings!
The error you WILL meet on day one: Book | undefined What does books.find(b => b.id === id) return when nothing matches? undefined (you know this from Β§9). So TS says its return type is Book | undefined β€” and refuses book.copies until you've handled the undefined case:
let book = books.find(b => b.id === id);
book.copies;                    // ❌ 'book' is possibly 'undefined'

if (!book) return;              // handle the miss (guard clause β€” your addBook pattern!)
book.copies;                    // βœ… TS watched the if β€” inside here it KNOWS it's a Book
This isn't TS being annoying β€” it's the findIndex β†’ -1 β†’ splice deletes the wrong book hole I mentioned, found automatically. TS forces the boundary thinking you've been practicing all along.
Narrowing β€” the checker follows your ifs TS tracks control flow: after if (!book) return; the type narrows from Book | undefined to Book. Same with typeof x === "string" branches. Your guard-clause habit is exactly how narrowing is done β€” you were writing TS-shaped code before you knew TS.

7. any β€” the type that turns TypeScript off

any means "don't check this." Every safety net in this page β€” gone, for that variable, and for everything it touches.

let data: any = "hello";
data.lenght;        // no error. data() β€” no error. data.foo.bar β€” no error. All bombs at runtime.
Our goal: ZERO any (the plan literally says so) any spreads: one any assigned into other variables silently makes them unchecked too. With "strict": true, TS at least flags implicit anys (untyped parameters). When a type is genuinely unknown (e.g. JSON.parse output), the honest tool is unknown: TS then forces you to CHECK before using β€” vs any which lets you do anything blind. See a lazy tutorial slap any on things? That's the tutorial giving up, not a technique.

8. TS meets the DOM β€” two speed bumps in your renderBooks/addBook

Converting the Library, you'll hit these immediately β€” they're features, not obstacles:

Bump 1: getElementById might find nothing Its return type is HTMLElement | null β€” because a typo'd id really does return null (remember the script-before-tbody crash in Β§14? Same hole). Guard it, or assert when you're certain:
let tbody = document.getElementById("bookRows");
if (tbody) { tbody.innerHTML = rowsHTML; }        // narrowed, safe

// or, when you KNOW the element exists in your HTML:
let tbody = document.getElementById("bookRows")!;  // ! = "trust me, not null"
Bump 2: .value doesn't exist on HTMLElement getElementById can't know it found an input, and plain HTMLElements have no .value. Tell TS which element it is with as (a type assertion):
let input = document.getElementById("newTitle") as HTMLInputElement;
input.value;    // βœ… now TS knows .value exists β€” and that it's a string
And because TS knows .value is a string, forgetting Number(...) on copies becomes a compile error the instant you try to put it in a number slot. The "+ trap" from Β§5 β€” extinct.

9. Generics β€” the <T> you keep seeing (light intro)

How can one find work on arrays of books AND arrays of numbers, yet stay type-safe? Generics: a type placeholder, filled in per use.

// Book[] is shorthand for:
let books: Array<Book> = [];

// on Book[], TS reads find as:  find(callback): Book | undefined
// on number[], the SAME find is: find(callback): number | undefined
Read <T> as "of ___" Array<Book> = "array of Books." T is a blank the user of the function fills; TS then threads that type through parameters and return. That's the whole trick β€” a type-level parameter, exactly like function parameters but for types. For now just read them; writing your own generics comes later.

10. The conversion plan β€” Library JS β†’ TS

This is the actual Phase 3 project, in order:

  1. Extract β€” move the inline <script> body into app.ts; point the HTML at the compiled app.js.
  2. Compile dirty β€” run tsc, watch it list errors. Don't panic: the errors are a TODO list, and each one is a real hole JS was hiding.
  3. Write interface Book β€” then let books: Book[] = [...]. Most errors organize themselves around this.
  4. Type every function signature β€” borrowBook(id: number): void, etc.
  5. Fix the find/null/DOM bumps β€” guards, as HTMLInputElement, handle Book | undefined.
  6. Zero errors, zero any, app works identically β€” that's the Phase 3 finish line.
localStorage returns a lie-shaped value JSON.parse(saved) returns any β€” TS can't know what was in storage (this is where your stale-id migration bug lived, Β§27!). The honest pattern: books = JSON.parse(saved) as Book[]; β€” an assertion, i.e. a promise you make. TS can't verify what's on disk; you can at least pin the expected shape.

11. The mental model to keep

JavaScriptTypeScript
Types exist, but only in your headTypes written down, checked by machine
Bugs surface at run time, far from the causeBugs surface at compile time, at the cause
Silent undefined / NaNLoud red squiggle, before you even save
Shape of a Book: tribal knowledgeShape of a Book: interface, enforced + autocompleted
Runs in the browserCompiles TO the JS that runs in the browser
One sentence to remember TypeScript is the discipline you've been learning by getting burned β€” automated. Guard clauses, boundary checks, convert-at-the-door, one source of truth: every habit this project taught you the hard way, TS enforces for free. That's why we learned JS first β€” you know exactly which fires this alarm system prevents.

12. The conversion, as it actually went (2026-08-27) βœ…

Your real Phase 3 run β€” 18 errors to zero, with what each step taught:

StepErrorsWhat happened
First tsc on raw JS184 buckets: untyped params (TS7006 Γ—4), possibly-null DOM (TS2531 Γ—6), .value missing on HTMLElement (TS2339 Γ—5), find-may-be-undefined (TS18048 Γ—3). The errors were a TODO list, exactly as promised.
interface Book + books: Book[]18 β†’ 14One interface killed 4 errors at once β€” inference flowed through Book[] into every callback's (book) param. Types at the source spread to the users.
if (!book) return; guard14 β†’ 11Narrowing: after the guard, Book | undefined becomes Book. The id-99 story β€” a find that misses returns undefined, and TS refuses to let you touch it unguarded.
First DOM pass11 β†’ 12 ⚠️Errors went UP: a leftover experiment line + only a partial fix. Lesson: read the compiler output as evidence, not as a score. The list tells you exactly which lines β€” believe it.
as HTMLInputElement Γ—5 + !12 β†’ 0 πŸŽ‰Zero errors, zero any. Then the Β§2 exports-crash detour (tsconfig, above) β€” and finally: all 5 features verified working, behavior identical.
What the error count really measured All 18 were holes JavaScript had been silently tolerating the whole time. The app "worked" before only because you hadn't yet stepped in those holes with the wrong foot. Conversion didn't change what the app does β€” it changed how much of it is proven.

13. Capstone: Loans, due dates & fines β€” first NEW code in TS

Everything so far was converting code you'd already written. The capstone is different: designing a feature in TypeScript from the start β€” types first, code second. Read-aheads: JS notes Β§29 (Dates) and Β§30 (linking by id) are the raw material.

The design idea: a Loan is a RECORD OF AN EVENT, not a change to a Book Wrong instinct: add borrowedBy/dueDate fields onto Book. That breaks the moment two copies of the same book are borrowed by different people. Right design: a separate Loan object per borrow event, pointing at its book by bookId β€” the same way your Delete buttons already send ${book.id}. One book ⇄ many loans. This is exactly a foreign key, the idea Phase 5 (SQL) is built on.
interface Loan {
    id: number;          // the loan's own id (same Math.max trick as books)
    bookId: number;      // WHICH book β€” the link, like a foreign key
    memberName: string;
    borrowedDate: string;  // ISO string β€” see the why box below
    dueDate: string;
    returned: boolean;
}
Why string dates and not Date? Loans live in localStorage via JSON.stringify β€” and JSON has no Date type. A Date object silently becomes a string on save, and JSON.parse gives you back a plain string, NOT a Date (JS notes Β§29). If the interface claimed Date, it would be lying after every reload. Honest rule: store ISO strings, wrap in new Date(...) only at the moment you need date math (comparing, adding days).
The date toolkit (from JS Β§29) new Date() = now Β· d.toISOString().slice(0, 10) β†’ "2026-08-28" Β· add 14 days: d.setDate(d.getDate() + 14) Β· compare: ISO strings sort/compare correctly as plain strings ("2026-09-01" > "2026-08-28" is true) β€” that's WHY ISO format puts year first.
What "Borrow" becomes Until now Borrow = copies - 1. In the capstone it becomes a transaction touching two arrays: decrement the book's copies and push a new Loan (who, which book, due when). Return does the reverse: mark the loan returned, increment copies. Fine = if today > dueDate, days late Γ— rate. Two sources of truth (books, loans) that must stay consistent β€” the exact problem real databases solve with transactions. You're meeting it small first.

14. Capstone, as it actually went (2026-08-30) βœ… β€” PHASE 3 COMPLETE

Built in one session: interface Loan + persistent loans array → Borrow creates a Loan (name via prompt, due = today + 14 days) → loans table with id→title lookup → Return button (copy goes back) → fines at ₹5/day overdue. Verified end-to-end: back-dated loan showed ₹50 (10 days late), Return flipped it to ₹0 and restored the copy, everything survived refresh. These are the bugs the build actually hit — each one is a permanent lesson:

Gotcha #1 you hit β€” writing code in app.js (the generated file) The whole Borrow-creates-Loan feature was typed into app.js β€” the file tsc generates. One more compile and the bulldozer would have erased it all. The same trap bit three times in one session from both sides: editing .ts and forgetting tsc (browser runs stale code), and editing .js directly (work gets overwritten). The chain is law: edit app.ts β†’ tsc β†’ refresh. app.js is disposable output β€” never open it to edit, only to read what the compiler produced.
Gotcha #2 β€” the live localStorage.clear() (last session's unsolved mystery) A leftover debugging line β€” localStorage.clear();, uncommented, at the bottom of the script β€” ran on every page load and wiped everything that had been saved. Symptom: deleted books kept reappearing after reload. This was also the real cause of the previous session's "clear()/delete not working" confusion. Lesson: scratch lines that destroy state are not harmless graveyard comments β€” they run. Delete debug lines the moment the experiment ends.
Gotcha #3 β€” if (!index) return; blocked deleting the FIRST book findIndex answers with a position: 0 for the first item, -1 for "not found". But !0 is true β€” so the guard treated the first book as "not found" and returned. Worse, !(-1) is false, so a real miss would sail through to splice(-1, 1) β€” which deletes the LAST item. The guard was wrong in both directions. Truthiness cannot tell "nothing" from "position 0" β€” compare against the actual sentinel: if (index === -1) return;
Gotcha #4 β€” the = vs === bug RETURNED (twice, in returnBook) loans.find(l => l.id = loanId) and books.find(b => b.id = loan.bookId) β€” the exact bug from the JS phase (notes Β§27), reborn. One = assigns: the "search" overwrites every id it touches. And here's the sober part: TypeScript did not catch it β€” assignment inside an arrow is legal code that returns a number, and find accepts it. Some bugs only discipline catches. When typing a condition, hear it in your head: "double-equals-strict compares, single assigns."
Gotcha #5 β€” loan.returned = "Returned" (data vs display, again) The interface says returned: boolean β€” the data stores true/false, never prose. The word "Returned" is presentation, produced at render time by loan.returned ? "Returned" : "Out". Same split as the very first render lesson: data stays clean and machine-shaped; the screen translates it for humans. (And this one TS did refuse β€” a string is not assignable to boolean. That's the conversion paying rent.)
Gotcha #6 β€” Return decremented copies (copy-paste without flipping intent) book.copies = book.copies - 1; copied straight from borrowBook into returnBook. Borrow takes a copy OUT (βˆ’1); Return brings it BACK (+1). As written, one borrow + return cost the library two copies. Lesson: when copying a line, re-ask what the story says the line should do here β€” the syntax travels, the intent doesn't.
The spine, extended: TWO arrays now, every change ends save + render β€” for BOTH that changed borrowBook touches books (copiesβˆ’1) AND loans (push) β†’ saveBooks(); saveLoans(); renderBooks(books); renderLoans();. returnBook likewise. Forgetting saveLoans() made loans vanish on refresh (in memory β‰  on disk β€” the oldest lesson in the app); forgetting renderLoans() made new loans invisible until reload (data β‰  screen). Two sources of truth that must stay consistent is exactly the problem SQL transactions solve β€” Phase 5 will feel familiar.
Fines are COMPUTED, never stored No fine field exists on Loan. Every render recomputes it fresh from the dates: returned β†’ β‚Ή0; not yet due β†’ β‚Ή0; else Math.floor((today βˆ’ due) in ms Γ· 86,400,000) days late Γ— β‚Ή5. Subtracting two Dates gives milliseconds, so divide by 1000Β·60Β·60Β·24 to get days. Why not store the fine? A stored fine is stale the next day; a computed one is always right. Rule of thumb: store facts (dates), derive opinions (amounts).
Why β‚Ή showed as Γ’β€šΒΉ β€” and why every page needs <meta charset="UTF-8"> Files are bytes, not characters. β‚Ή is three bytes in UTF-8; with no charset declared, the browser guessed an old one-byte-per-character encoding and read them as three separate letters: Γ’ β€š ΒΉ. The first line inside <head> β€” <meta charset="UTF-8"> β€” tells the browser how to decode the bytes. Every real webpage carries it; now the Library does too.
Console tricks learned while testing Functions wired to buttons work from the console too: borrowBook(1) pops the prompt. Fake test data the same way: loans[loans.length - 1].dueDate = "2026-08-20T00:00:00.000Z"; saveLoans() β†’ refresh β†’ the fine math runs against a 10-day-late loan. Predict the number BEFORE looking (10 Γ— β‚Ή5 = β‚Ή50) β€” a test you didn't predict is just watching, not testing.

← Prev: JavaScript  Β·  Back to contents  Β·  Next: Node.js β†’