TypeScript
The model
- TypeScript is JavaScript plus static types. The compiler checks them before the code runs, then erases them, so the JavaScript that runs has none.
- Types are structural. A value fits a type when it has the members the type asks for, whatever its class or name.
- Types are gone at runtime, so
asand interfaces can't check any data. A value from an API orJSON.parseneeds a real check in code. - On a union such as
string | number, you can use only what every member shares. A check liketypeofnarrows the type inside its branch. - Under strict mode,
nullandundefinedare separate types, so the compiler makes you handle a missing value.strictis on by default since TypeScript 6.0.
Types & inference
annotate what you must, let the compiler infer the restAnnotate or infer
f(x: string): numberstring[]Array<string>let n = 42Infers numberconst d = "up"The literal "up". let gives stringTrapUnder strict, a parameter needs a type unless it has a default or gets one from context, as a callback does.
Primitive and special types
string, number, boolean, null, undefined, symbol, bigintx: unknownNarrow it before usex: anyOnly for short-term migrationTrapany spreads. Anything you read from an any value is any too, so one any can silence checks far from where it started.
Tuples, literals and enums
[string, number]"up" | "down"enum Dir { Up, Down }A real object at runtimeTipA union of string literals gives an enum's checks with no runtime code. Use an enum when you need an object to loop over.
Objects & interfaces
describe what an object must haveInterfaces, optional and readonly
interface User { id: number }email?: stringReads as string | undefinedreadonly id: numberReadonly<User>Trapreadonly is compile-time and shallow. Nothing freezes the object, and a nested object's fields can still change.
Interface or type alias
type Id = A | BOnly type caninterface B extends Atype C = A & BTipThe handbook's rule is to use interface until you need a feature only type has.
TrapWhen two parent types disagree on a property's type, extends reports an error. An intersection quietly makes it never.
Index signatures and Record
{ [key: string]: number }Record<Day, string>A missing or extra key is an errorTrapReading a key that isn't there still gives the value type, not undefined. Turn on no to get T | undefined.
Function types and overloads
(a: number) => string(a: string, b?: number){ (s: string): void; level: number }f(x: string): number;Signatures, then one bodyTrapstrict checks parameters strictly only in function syntax. Method syntax like add(a: number): number escapes it.
Unions & narrowing
hold one of several types, then find out whichUnions and narrowing
A | BA & Btypeof x === "string"x instanceof Date"swim" in x(x: unknown): x is Userasserts v is stringTrapThe compiler trusts a type predicate without checking its body. A guard that says yes to the wrong values hides bugs.
Discriminated unions and never
{ kind: "circle"; r: number }const x: never = shapeIn the default branchfail(): nevervoid still returnsHandle null and undefined
user?.email ?? ""?? keeps 0, || doesn'tuser.email!NonNullable<T>Trap! does no runtime check. If the value is missing, the code still throws.
Assertions and satisfies
el as HTMLCanvasElement <HTMLCanvasElement>el Doesn't work in .tsx filesconfig satisfies ConfigKeeps its own type (4.9+)Trapas checks nothing at runtime. A wrong assertion compiles and fails later, so prefer a type guard.
Classes & decorators
fields, access, inheritance and decoratorsClasses and access modifiers
constructor(private id: number) {}privateprotectedClass, or class and subclasses#balance = 0Trapprivate is checked only at compile time, and plain JavaScript can still read the field. Use # for real privacy.
Extend, implement and abstract classes
class B extends ACall super() before thisclass A implements B, CMany allowed, but one extendsabstract area(): numberSubclasses must implement itTrapimplements only checks the class. It doesn't type the method parameters, so they still need annotations.
Static members, getters and setters
static PI = 3.14Read as MathUtils.PIget c(): numberset c(v: number)TrapStatic members can't use the class's type parameter T. One static side serves every instance type.
Decorators
experimentalDecorators NestJS uses them. They don't mix with TC39@Log("debug")A factory returns the decoratorTipA legacy property decorator runs once per class, not once per object, and never sees the initial value.
TrapA class decorator can add members at runtime, but the type system never sees them, so callers need as any.
Generics
write code once and keep the exact typesGeneric functions and constraints
identity<T>(x: T): TThe call infers TT to have a property<T extends { length: number }><K extends keyof T>Returns T[K]Generic interfaces and classes
interface Box<T> { value: T }<T = unknown>Queuenew Queue<number>()Nothing infers T, so write itCovariance and contravariance
T as input and outputInvariant. Neither direction fits<out T><in T>TrapMethod parameters stay bivariant, accepted in both directions, even under strict.
Asked in interviews
AskedWhat are covariance and contravariance in TypeScript?Utility & mapped types
build new types from the ones you haveUtility types
Partial<User>Fits an update payloadPick<User, "id">Omit<User, "password">ReturnType<typeof f>Parameters<typeof f>Keys, lookups and union filters
keyof Personkeyof typeof objPerson["name"]Users[number]Exclude<T, "a">Extract<T, "a">Mapped, conditional and template types
[K in keyof T]: string-? removes the ?[K in keyof T as N]Map to never to drop oneT extends U ? X : Yinfer E captures a part`on${Capitalize<E>}`typeof arr[number]After as constTipRecursive types like DeepReadonly call themselves. Deep recursion slows the compiler and can hit its instantiation limit.
TrapOn a bare type parameter, a conditional type runs per union member. Wrap both sides, [T] extends [U], to stop it.
Compiler & tsconfig
compile, configure and resolve importsCompile, check and run
tsctsc --watchtsc --noEmitnode app.tsStrips types, no type checkTrapVite, esbuild and SWC strip types without checking them. Run tsc --noEmit in CI so type errors fail the build.
tsconfig.json and strict mode
"target": "ES2020""lib": ["ES2020", "DOM"]"extends": "./base.json"Pick one with tsc -p"strict": trueIncludes strictNullChecks Modules and namespaces
export const x = 1import { x } from "./m"import type { User }Removed from the outputnamespace Geo {}Use ES modules in new codeTipBundlers drop a module's unused exports. A namespace compiles to one object they can't trim.
TrapA file with no top-level import or export is a global script, not a module.
Module resolution and .d.ts files
moduleResolution "bundler""nodenext"For bundled apps, or Node.js"paths": { "@/*": ["src/*"] }node mode and baseUrl fail in 7.0npm i -D @types/lodashAnd @types/node for Nodedeclare module "lib"In a types-only .d.ts fileTrappaths only tells the compiler. The bundler or runtime needs the same alias, or the import fails when it runs.
Debug and speed up builds
"sourceMap": truenode --enable-source-maps app.jsExpand<T>"skipLibCheck": true"composite": truetsc --buildTipSplit a long type into named steps and test each one with a literal. no shows whole types.
React, async & API data
components, promises, errors and data you receiveReact with TypeScript
({ label }: ButtonProps)useState<User | null>(null)ChangeEvent<HTMLInputElement> And FormEvent for a submit{ type: "add" }A union tagged by typeTraptarget is where the event started, which can be a child. Use currentTarget for the element the handler is on.
Async code and API data
await Promise.all([a(), b()]){ success: true; data: T }Schema.safeParse(data) z.infer<typeof Schema>Trapresponse.json() and JSON.parse return any. Typing the result as User checks nothing at runtime.
Errors and Result types
err instanceof ErrorIt's unknown under strictextends ErrorSet name so logs show itResult<User, string>Callers check successTrapTypeScript doesn't track what a function throws. Nothing makes a caller handle an exception.
Typed events and messages
{ "user:created": { id: string } }emit<K extends keyof E>(e: K, data: E[K])Extract<Msg, { type: T }>Migration & large codebases
migrate, test, organise and stay type-safe at scaleMigrate from JavaScript
"allowJs": true"checkJs": true// @ts-checkTypeScript reads JSDoc typescheckJs, noImplicitAny, then strictNullChecks Habits and anti-patterns
Pick, Omit, typeof and keyof// @ts-expect-errorErrors once fixed, unlike @ts-ignorestring & { __brand: "UserId" }A branded typeTipTurn on strict from the start, and prefer a union of strings to an enum.
TrapTwo types with the same shape are interchangeable. A UserId and an OrderId that are both string mix freely.
Team rules and typed tests
"extends": "../tsconfig.base.json" any in lint@typescript-eslint/no-explicit-any (overrides: Partial<User>): UserBreaks when the type changesBig projects and shared contracts
APIv1.UserAPIv2.UserOne function maps old to newTrapA breaking change to a shared types package makes every service update together, which undoes independent deploys.
Bundle size and runtime cost
import debounce from "lodash/debounce" React.lazy(() => import("./Chart")) TrapNamed imports from lodash still pull in its one CommonJS file. Use lodash-es or per-method paths.
Design patterns
private constructor()Plus a static getInstancethis: Req<"idle">A phantom type parameterOption type with map, flatMapTrapA DI container can't find a dependency by its interface, because interfaces vanish at runtime. It maps tokens instead.
Asked in interviews
AskedHow do you implement design patterns with TypeScript?Nothing matches. Try a shorter word, or clear the search.
This sheet stays free. With Pro, you can:
- Every answer on this sheet. Read the full answer to all 102 questions it links, not just the 16 free ones.
- My sheet. The cards you star and the ones you miss in Quiz me, from every sheet, on one page to quiz and print.
- PDFs. Download this sheet and My sheet, laid out for A4, with every question a link to its answer.
- The rest of Pro. Every answer in the question library, and 1,000 AI credits a month.