TypeScript, just enough
The whole stack is TypeScript. Learn what static types are, the bugs they catch before you run the code, and how they make both you and the AI faster.
Nearly every codebase you’ll build in — including this one — is TypeScript: JavaScript with a type checker bolted on. You don’t need to master the type system; you need enough to read it, to know what types buy you, and to know that types make the AI dramatically more accurate. That’s this chapter.
The bug types catch before you run
Dynamic JavaScript happily lets you misspell a field or pass the wrong shape — you find out at runtime, in production, on a real user. Types move that discovery into your editor, where it costs you a red squiggle instead of an incident.
// JavaScript — runs fine, then crashes a user at runtime
function total(order) {
return order.price * order.quantty; // typo: quantty → NaN, ships to prod
}// TypeScript — the typo is a red squiggle before you ever run it
interface Order { price: number; quantity: number; }
function total(order: Order): number {
return order.price * order.quantty;
// ~~~~~~~ Property 'quantty' does not exist on type 'Order'.
}The type annotation order: Order is a contract: this function promises to take
an Order and return a number. Break the contract and the checker — and the AI’s
editor — flags it instantly.
Reading and writing a basic type
You’ll be reading types far more often than writing them. Here’s the 20% that covers most of what you’ll see — primitives, a named shape, an array, a union, and an optional field — all in one block:
// primitives
let name: string;
let count: number;
let done: boolean;
// a named shape (interface), with an optional field
interface User {
id: number;
name: string;
email?: string; // the ? means email may be missing
}
// an array of that shape
const users: User[] = [];
// a union: the value is exactly one of these literals
type Status = "todo" | "doing" | "done";
// a function's typed contract: takes a User, returns a Status
function statusFor(u: User): Status {
return u.email ? "done" : "todo";
}
The union Status is quietly powerful: if you later write a switch over a status
and forget the "doing" case, the checker can flag it. The optional email? forces
the u.email ? … check — TypeScript won’t let you treat a maybe-missing value as
definitely-present.
Inference: why you don’t annotate everything
A common beginner reflex is to annotate every variable. You don’t need to —
TypeScript infers most types from the value. const n = 5 is already number;
const names = ["a", "b"] is already string[]. Annotate the boundaries —
function parameters, return types, and the shape of data crossing into your code from
an API or database — and let inference handle the interior.
Why types make the AI better
This is the part most people miss: types aren’t just for you. When the AI can see
that a function returns User[], it stops inventing fields, auto-completes the real
ones, and catches its own mistakes in the editor before you ever run them. Types are a
shared contract the model reads exactly as you do.
| You say to the AI | What it does | Why it helps |
|---|---|---|
| “Add types to this file” | annotates args, returns, and shapes | turns silent runtime bugs into editor errors |
| “Make this strict” | enables strict null checks etc. | forces handling of null/undefined |
| “Type this API response” | writes an interface for the JSON | the AI now knows every field's name + type |
01 Learning objectives
0 / 6 done02 Curated reading
03 Knowledge check
- 01easy
TypeScript catches a misspelled field…
- 02easy
In TypeScript you must annotate the type of every variable.
- 03medium
Why do types make an AI assistant more accurate?
- 04medium
A signature like total(o: Order): number is best described as…
04 Interview questions
browse all ↗What gets asked on this topic — tap a card for how to approach it, the follow-ups, and the trap. Company tags are best-effort & sourced.
-
What problem does TypeScript solve over plain JavaScript? What class of bugs does it catch?
TypeScript adds static types checked at compile time, so a whole class of bugs is caught before the code runs: typos in property names, passing the wrong shape, calling a method that doesn't exist, forgetting a required field, or assuming a value is present when it can be
undefined.It's a developer-time tool — the types are erased and it's plain JS at runtime. The payoff is the error shows up in your editor as you type instead of as a crash in production.
Follow-ups they push on- Do types exist at runtime?
- Does TypeScript make code faster?
Red flag Believing TS catches every bug — it catches type/shape errors, not logic errors. `if (x = 5)` is still wrong; a bad algorithm is still bad.
source: TypeScript — TS for JavaScript Programmers ↗ -
What's the difference between `interface` and `type` in TypeScript?
Both describe the shape of data.
interfaceis best for object shapes and class contracts — it can be extended and merged (declaration merging).typeis a more general alias — it can do everything interface does for objects plus unions, intersections, primitives, and tuples.Practical rule: reach for
interfacefor object shapes you might extend, andtypewhen you need a union or a non-object alias. At orientation level the honest answer is they overlap heavily and either is fine for object shapes.Follow-ups they push on- Which can express a union type?
- What is declaration merging?
Red flag Claiming they're identical — `type` can express unions and primitives; `interface` supports declaration merging.
source: DataCamp — TypeScript Interview Questions ↗ -
What's the difference between `any` and `unknown`?
anyturns type checking off for that value — you can do anything with it and TS won't complain, which throws away the safety you came for.unknownis the safe counterpart: you can hold any value, but you must narrow it (check its type) before you use it.Rule of thumb:
anyis an escape hatch (use sparingly, e.g. migrating JS);unknownis a checkpoint that forces you to prove the type first. Preferunknownwhen you genuinely don't know the type yet.Follow-ups they push on- Why is `unknown` safer for a parsed JSON / API response?
- When is reaching for `any` defensible?
Red flag Sprinkling `any` to silence errors — it defeats the point of TypeScript and hides real bugs. Tighten the type instead.
source: DataCamp — TypeScript Interview Questions ↗ -
What is type inference, and why don't you annotate every variable?
Type inference is TypeScript figuring out the type from the value automatically: write
const n = 5and TS knowsnisnumber— no annotation needed.You skip redundant annotations because they add noise without adding safety. Annotate where inference can't help or where you want to pin a contract: function parameters, function return types for public APIs, and the shape of external data (API responses). Let inference handle the obvious local cases.
Follow-ups they push on- Where is an explicit annotation still worth it?
- What does `const x: number = 5` add over `const x = 5`?
Red flag Annotating everything 'to be safe' — over-annotation is noise; the value is typing boundaries, not every local.
source: TypeScript Handbook — Everyday Types ↗ -
How do types make an AI coding assistant more useful?
Types are machine-readable context. With a typed codebase the assistant gives better completions (it knows the exact shape available), invents fewer non-existent fields (the contract is right there), and its mistakes surface as in-editor type errors instead of silent runtime bugs.
So a typed contract is a form of guardrail for the AI: it constrains what valid code looks like, which is why 'add types' or 'type this API response' is a high-leverage instruction to give it.
Follow-ups they push on- What does telling the AI to 'make this strict' do?
- Why does a typed API response reduce hallucinated fields?
Red flag Treating types as only a human concern — they're also the strongest signal the model has about valid code.
source: TypeScript — TS for JavaScript Programmers ↗ -
Name TypeScript's main primitive types and how you type an array and an object.
The core primitives are
string,number,boolean, plusnullandundefined(andbigint/symbolyou rarely touch early). You type an array asnumber[](orArray<number>), and an object by its shape:{ name: string; age: number }.A point that trips JS devs: TypeScript has no separate
int/float— it's allnumber. Andstring[]means 'array of strings', whilestringalone is one string. Get comfortable reading these shapes; most real-world typing is just composing primitives into object and array shapes.What a strong answer coversPrimitives:
string,number,boolean,null,undefined(plusbigint,symbol).No
int/floatdistinction — all numbers arenumber.Array:
T[]orArray<T>(e.g.string[]).Object: describe its shape —
{ name: string; age: number }.
Follow-ups they push on- What's the difference between `number[]` and `[number, number]`?
- How do you mark an object field as optional?
Red flag Looking for `int`/`float`/`char` types from other languages — TypeScript only has `number` and `string`; there's no character type.
source: TypeScript Handbook — Everyday Types ↗ -
What is a union type, and how do you write a literal union for something like a status field?
A union type says a value is one of several types, written with
|:string | numbermeans 'either a string or a number'. The most useful flavor is a literal union of exact values:type Status = "idle" | "loading" | "error" | "done".This is huge for modeling state: instead of a loose
stringthat could be any typo, the type pins the field to exactly the allowed values, sostatus = "loadign"is a compile error and your editor autocompletes the valid options. It's the cleanest way to make impossible states unrepresentable.What a strong answer coversA union (
A | B) means the value is one of the listed types.A literal union (
"a" | "b" | "c") restricts to exact allowed values.Great for status/role/variant fields — typos become compile errors.
Editor autocompletes the valid options, so you can't pick an invalid one.
Quick self-checkWhich type best models a button's variant, which must be exactly 'primary', 'secondary', or 'ghost'?
-
Wrong — allows any string, including typos like 'primry'; no autocomplete.
-
Correct — a literal union pins it to exactly the three valid values, catching typos at compile time.
-
Wrong — turns off checking entirely; the worst choice for a fixed set.
-
Wrong — that's an array of strings, not one of three allowed values.
Follow-ups they push on- How does TypeScript 'narrow' a union so you can use it safely?
- Why is a literal union better than a plain `string` for a status field?
Red flag Typing a fixed-set field as `string` — you lose the typo-catching and autocomplete a literal union would give you for free.
source: TypeScript Handbook — Everyday Types (Union Types) ↗ -
What's the difference between optional (`?`) and `| undefined`, and how do you safely read a value that might be missing?
field?: stringmeans the property may be absent entirely (you can omit the key).field: string | undefinedmeans the key must be present but its value may beundefined. They overlap a lot in practice; the practical concern is the same — you must handle the missing case before using it.To read it safely, use narrowing: an
if (user.name)check, optional chaininguser.profile?.bio, or a default with??(const name = user.name ?? "Anonymous"). WithstrictNullCheckson, TypeScript forces you to do this — it won't let you call.toUpperCase()on something that might beundefined, which kills a huge class of 'cannot read property of undefined' crashes.What a strong answer covers?= the property may be absent;| undefined= present but possibly undefined.Both require handling the missing case before use.
Narrow with
ifchecks, optional chaining?., or nullish coalescing??.strictNullChecksmakes the compiler force this — preventing undefined-access crashes.
Follow-ups they push on- What does optional chaining (`?.`) return when the left side is undefined?
- Why does `strictNullChecks` catch so many real-world bugs?
Red flag Accessing a possibly-undefined value directly (`user.name.toUpperCase()`) — without narrowing it crashes at runtime; let strict mode force the check.
source: TypeScript — Migrating with strictNullChecks / handling null ↗ -
TypeScript said the code is type-correct, but it still crashed at runtime with bad data from an API. How is that possible?
TypeScript types are erased at compile time — they don't exist at runtime and don't check actual values. When you write
const user = await res.json() as User, you're *asserting* the shape, not verifying it. If the API returns something different, TS believed your assertion and the mismatch only surfaces as a crash later.Types guarantee your code is internally consistent; they cannot police data that enters at runtime (API responses, form input, JSON files). For real boundaries you need runtime validation — a schema validator like Zod that actually checks the value and *then* gives you a trustworthy type. Static types and runtime validation are different jobs.
What a strong answer coversTypes are erased at compile time — no runtime checking of actual values.
as Useris an unchecked assertion; TS trusts you, it doesn't verify.External data (APIs, forms, files) can violate the asserted type silently.
Validate at the boundary with a runtime schema (e.g. Zod) to get a type you can trust.
Quick self-checkYou write `const data = await res.json() as Product`. The API changes and now omits `price`. What happens?
-
Wrong — TS can't see the runtime response; the assertion is taken on faith.
-
Correct — `as` is unchecked and types are erased, so the mismatch isn't caught until you use the missing field.
-
Wrong — `res.json()` parses JSON; it has no knowledge of your TypeScript type.
-
Wrong — `as` performs no runtime validation; that's the entire trap.
Follow-ups they push on- Why is `as SomeType` on an API response dangerous?
- How does a tool like Zod give you both a runtime check and a static type?
Red flag Using `as` to assert the shape of external data and assuming it's now safe — `as` does no checking; only runtime validation actually verifies the value.
source: TypeScript Handbook — Type assertions (`as`) ↗ -
How do shared types act as a contract between your frontend and backend?
If both sides of your app are TypeScript, you can define the shape of the data once —
interface User { id: string; name: string; email: string }— and import it in both the API code and the frontend. That shared type is a contract: the server is typed to return it, the client is typed to consume it.The payoff is compile-time safety across the boundary. If you rename
nametofullNameon the server but forget the frontend, the build breaks at the mismatch instead of shipping a silently broken page. It turns 'did the API change?' from a runtime surprise into a type error you see immediately — the single biggest reason teams run TypeScript end to end.What a strong answer coversDefine the data shape once; import it on both client and server.
The shared type is an enforced contract across the API boundary.
A change on one side that breaks the other fails the build, not production.
Turns API drift from a runtime surprise into an immediate compile error.
Follow-ups they push on- What happens at build time if the server's response no longer matches the shared type?
- How do tools generate these shared types from an API schema automatically?
Red flag Hand-redeclaring the same shape separately on client and server — they drift out of sync; share one source-of-truth type instead.
source: TypeScript Handbook — Object Types (interfaces) ↗