TypeScript — Types, Generics and Real-World Patterns
In short: A typed superset of JavaScript that compiles to plain JavaScript.
- Static types catch errors at compile time, give IntelliSense and safe refactoring, document APIs and scale better for large teams.
- Types are erased at runtime, so there is no runtime cost — and no runtime checking.
In short: Both describe object shapes.
typecan also name unions, intersections, tuples, primitives and mapped/conditional types;interfacesupports declaration merging andextends, and is often preferred for public object contracts.- Interfaces can't express unions; types can't be reopened.
In short: any disables type checking for that value (errors pass silently).
unknownaccepts any value but must be narrowed (typeof/guards/schema) before use — the safe choice for external data andcatcherrors.neverrepresents values that never occur — functions that always throw, or the impossible branch in an exhaustive switch.
In short: A union A | B means the value is one of several types.
- Narrow with
typeof,instanceof,in, equality on a discriminant property (kind), truthiness, or custom type guards (x is T) so TypeScript knows which member you have in each branch.
In short: The compiler works out types without annotations: let n = 5 is number, a function's return type comes from its return statements, generic arguments come from the values passed.
- Annotate function parameters and public APIs; let inference handle locals.
In short: Type parameters that make code reusable while keeping types: function first<T>(xs: T[]): T | undefined.
- With constraints (
T extends { id: number }) andkeyof(K extends keyof T) you get type-safe helpers such aspluck(items, "name"). - Common built-ins:
Array<T>,Promise<T>,Record<K,V>.
In short: Partial<T> makes all props optional (update/PATCH payloads).
Pick<T,K>keeps chosen keys (summary DTO).Omit<T,K>removes keys (create payload withoutid).Record<K,V>builds an object type with keys K (lookup tables likeRecord<Status, string>).
In short: strict: true enables noImplicitAny, strictNullChecks, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, alwaysStrict, useUnknownInCatchVariables and others.
- It catches the most common bugs — especially null/undefined access.
- New projects should always use it; legacy projects can turn flags on gradually.
In short: A union of object types that share a literal "tag" property (kind, type, status).
- Switching on the tag narrows to the exact member, and a
nevercheck indefaultmakes the switch exhaustive — adding a new member produces compile errors wherever it isn't handled. - Ideal for state (
{ status: "loading" } | { status: "error"; error: string } | { status: "ok"; data: T }) and Redux/NgRx actions.
as (type assertion) and satisfies?In short: as T tells the compiler "trust me, it's T" — no runtime check and it can hide mistakes.
satisfies Tchecks that a value matches T but keeps the more precise inferred type, so you get validation without losing literal types.- Prefer
satisfiesfor config objects and avoidasexcept at well-validated boundaries.
In short: Files containing only type declarations that describe JavaScript code — they let TypeScript type-check calls into JS libraries.
- Libraries ship their own (
"types"in package.json) or come from DefinitelyTyped (npm i -D @types/lodash). - You can write
declare module "x"or augment globals (e.g.Window) in your own.d.ts.
In short: Mapped types transform each property of another type: { [K in keyof T]: Readonly<T[K]> } (with as to rename keys).
- Conditional types choose a type by condition: `T extends U ?
- X : Y
, withinferto extract parts (T extends Promise? - R : T`).
- Built-in utilities like
Partial,ReturnTypeandAwaitedare implemented with them.
In short: Structural: types are compatible if their shapes match, regardless of names, so { id: 1, name: "x" } is assignable to any interface with those members.
- C# and Java are nominal.
- To simulate nominal types (e.g.
UserIdvsOrderIdboth numbers), use branded types:type UserId = number & { readonly __brand: "UserId" }.
In short: Types are erased at runtime; http.get<Order[]>() or as Order[] only told the compiler what to expect.
- Prevent: generate client types from the backend OpenAPI document in the build (so contract changes show up as compile errors), validate responses at the boundary with Zod/Valibot, version the API, and add contract/end-to-end tests.
In short: Incrementally: add tsconfig with allowJs and checkJs off, rename files to .ts module by module starting from leaves (utilities, models), add @types packages, use any/unknown temporarily with TODOs, then turn on strict flags one by one (noImplicitAny, then strictNullChecks).
- Add type-checking (
tsc --noEmit) to CI so coverage only grows.
function groupBy<T, K extends PropertyKey>(items: T[], key: (item: T) => K): Record<K, T[]> {
return items.reduce((acc, item) => {
const k = key(item);
(acc[k] ??= []).push(item);
return acc;
}, {} as Record<K, T[]>);
}
const byStatus = groupBy(orders, o => o.status); // Record<OrderStatus, Order[]>(Modern runtimes also have Object.groupBy(items, fn) from ES2024.)