Chapter 82 β TypeScript Essentials for MERN
π Definition
TypeScript is a statically typed superset of JavaScript that compiles to plain JS. In a MERN app it lets you describe the shape of data once β a User, an Order, an API response β and have the compiler check every place that data flows: the Mongoose model, the Express handler, the fetch call, and the React component that renders it.
Types exist only at compile time. At runtime it is still JavaScript β which is why you also need runtime validation (express-validator or custom type guards β see Typed Express Server) at every trust boundary.
π Why TypeScript in a MERN Stack?
| Without TS | With TS |
|---|---|
user.emial β undefined at runtime |
Compile error: Property 'emial' does not exist |
Backend renames name β fullName, frontend silently breaks |
Shared type changes β every consumer goes red |
| "What does this endpoint return?" β read the code | Hover the function, see the type |
| Refactors are scary | Rename symbol across client + server safely |
Interviewers expect you to know why (safety, refactoring, self-documenting contracts) and the limits (no runtime guarantees, any leaks, type-only imports vanish).
π» Code Example β type vs interface
// interface β extendable, can be re-opened (declaration merging)
interface User {
id: string;
email: string;
}
interface User { // merges with the one above
role: "admin" | "user";
}
interface Admin extends User {
permissions: string[];
}
// type β can express unions, tuples, mapped & conditional types
type Role = "admin" | "user";
type ApiResult<T> = { ok: true; data: T } | { ok: false; error: string };
type Point = [x: number, y: number];Rule of thumb: interface for object shapes you expect to extend (and for augmenting libraries like Express's Request); type for unions, utility compositions and everything else. Both are fine β consistency matters more.
π» Code Example β Generics
// A generic paginated response used by every list endpoint
export interface Paginated<T> {
items: T[];
page: number;
limit: number;
total: number;
}
// Generic constraint: T must have an _id
function indexById<T extends { _id: string }>(list: T[]): Record<string, T> {
return Object.fromEntries(list.map((item) => [item._id, item]));
}
// Generic with default
type ApiResponse<T = unknown> = { success: boolean; data: T; message?: string };π» Code Example β Utility Types You Will Actually Use
interface User {
_id: string;
email: string;
name: string;
password: string;
role: "admin" | "user";
createdAt: Date;
}
type PublicUser = Omit<User, "password">; // response DTO
type CreateUserDto = Pick<User, "email" | "name" | "password">;
type UpdateUserDto = Partial<Omit<User, "_id" | "createdAt">>; // PATCH body
type ReadonlyUser = Readonly<User>;
type RoleCount = Record<User["role"], number>; // { admin: number; user: number }
type Fn = (id: string) => Promise<User>;
type FnReturn = Awaited<ReturnType<Fn>>; // User
type FnArgs = Parameters<Fn>; // [id: string]
type MaybeNull = string | null | undefined;
type Sure = NonNullable<MaybeNull>; // stringπ» Code Example β Narrowing & Discriminated Unions
type FetchState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: string };
function render(state: FetchState<User[]>): string {
switch (state.status) {
case "idle": return "Nothing yet";
case "loading": return "Loadingβ¦";
case "success": return `${state.data.length} users`; // data is available here
case "error": return state.error; // error is available here
default: {
const _exhaustive: never = state; // compile error if a case is missed
return _exhaustive;
}
}
}Other narrowing tools:
if (typeof x === "string") {} // typeof guard
if (err instanceof AppError) {} // instanceof guard
if ("data" in res) {} // in-operator guard
// Custom type guard
function isUser(v: unknown): v is User {
return typeof v === "object" && v !== null && "email" in v;
}
// Assertion function
function assertDefined<T>(v: T, msg: string): asserts v is NonNullable<T> {
if (v == null) throw new Error(msg);
}π» Code Example β any vs unknown vs never
let a: any = JSON.parse(raw);
a.foo.bar.baz(); // compiles β and crashes at runtime. any turns checking OFF.
let u: unknown = JSON.parse(raw);
// u.foo; // β error: must narrow first
if (typeof u === "object" && u !== null && "foo" in u) { /* ok */ }
function fail(msg: string): never { // never returns
throw new Error(msg);
}In catch blocks,
errisunknownunderstrict(useUnknownInCatchVariables). Narrow witherr instanceof Errorbefore reading.message.
π» Code Example β as const, satisfies, and Enums
// as const β literal, readonly types; great replacement for enums
export const ROLES = ["admin", "user", "guest"] as const;
export type Role = (typeof ROLES)[number]; // "admin" | "user" | "guest"
export const ORDER_STATUS = {
Pending: "pending",
Paid: "paid",
Shipped: "shipped",
} as const;
export type OrderStatus = (typeof ORDER_STATUS)[keyof typeof ORDER_STATUS];
// satisfies β checks the shape WITHOUT widening the inferred type
const routes = {
home: "/",
user: "/users/:id",
} satisfies Record<string, string>;
routes.user; // type is still the literal "/users/:id", and typos in keys are caughtWhy many teams avoid enum: numeric enums are not type-safe (Status.A = 99 compiles in older versions), they generate runtime code, and they don't work with erasableSyntaxOnly / Node's native type stripping. as const objects + union types give the same ergonomics with zero runtime cost.
π» Code Example β Mapped & Conditional Types (Senior-level)
// Make every field of T optional AND nullable
type Nullable<T> = { [K in keyof T]?: T[K] | null };
// Pick only the keys of T whose values are strings
type StringKeys<T> = { [K in keyof T]: T[K] extends string ? K : never }[keyof T];
type UserStringFields = StringKeys<User>; // "_id" | "email" | "name" | "password" | "role"
// Unwrap an array element
type ElementOf<T> = T extends readonly (infer E)[] ? E : never;
type R = ElementOf<typeof ROLES>; // "admin" | "user" | "guest"
// Template-literal types β typed event names
type Entity = "user" | "order";
type EventName = `${Entity}:${"created" | "deleted"}`; // "user:created" | ...π» Code Example β A Strict tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext", // server; use "ESNext" + "Bundler" resolution for Vite/React
"moduleResolution": "NodeNext",
"strict": true, // strictNullChecks, noImplicitAny, etc.
"noUncheckedIndexedAccess": true, // arr[0] is T | undefined
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"verbatimModuleSyntax": true, // forces `import type` for type-only imports
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "dist",
"rootDir": "src",
"sourceMap": true
},
"include": ["src"]
}import type { User } from "./types" is erased completely at compile time β important for avoiding circular runtime imports and for bundle size.
π Real-World Impact
- One shared
typespackage means a backend change produces compile errors in the React app before deploy, not bug reports after. - Discriminated unions make impossible UI states (loading and error) unrepresentable.
strict+noUncheckedIndexedAccesscatches the classicCannot read properties of undefinedat build time.
π― Likely Interview Questions
typevsinterfaceβ when do you use each? β Interface for extendable object shapes and declaration merging (augmenting ExpressRequest); type for unions, tuples, mapped/conditional types.anyvsunknown? βanydisables checking;unknownforces you to narrow before use. Preferunknownfor untrusted input.- What is a discriminated union and why use it? β A union with a shared literal field (
status) so TS narrows by checking it; enables exhaustiveswitchwithnever. - Explain
Partial,Pick,Omit,Record. β Give the DTO examples above. - What does
satisfiesdo differently from a type annotation? β Validates against a type but keeps the narrower inferred type. - Does TypeScript validate API input at runtime? β No. Types are erased; validate at boundaries with express-validator, class-validator (NestJS) or hand-written type guards.
- What is
neverused for? β Unreachable code, functions that throw, and exhaustiveness checks. - Why avoid
enum? β Runtime output, numeric-enum unsafety, incompatible with type-stripping; useas constunions.
β MongoDB Rapid-Fire Q&A | Index | Next: Project Setup & Architecture β