Cheat sheet · TypeScript From Zero to Confident

TypeScript From Zero cheat sheet

TypeScript from setup to strict migration: narrowing, objects and classes, generics, utility and mapped types, modules, runtime validation, and reading errors. Types are erased, so the checker never sees real data.

Read the full series →

blogs · jp-blogs.vercel.app/cheatsheets/series/typescript

What TypeScript isLesson 0 →

ToolChecks types?Use it for
tscYes (the only one)tsc --noEmit in CI and editors
tsx file.tsNoRun now (strips types)
node file.tsNoRun on Node 22.18+ (strips types)
  • TypeScript = JavaScript + a checker. interface, type and : string are deleted, so you cannot test them at runtime.
  • Node's stripping fails on enums, parameter properties and runtime namespaces. It ignores tsconfig.json, so paths do not work.

Watch out: JSON.parse(s) as { id: number } passes tsc and still crashes at runtime. as is an instruction, not a conversion.

A starter tsconfigLesson 0 →

{ "compilerOptions": {
  "target": "es2022", "module": "nodenext",
  "strict": true,
  "noUncheckedIndexedAccess": true,
  "skipLibCheck": true,
  "isolatedModules": true,
  "verbatimModuleSyntax": true,
  "erasableSyntaxOnly": true,
  "allowImportingTsExtensions": true,
  "noEmit": true, "types": ["node"] },
  "include": ["src"] }
strict
Null checks, noImplicitAny and more (default since TS 6)
noUncheckedIndexedAccess
arr[i] becomes T | undefined; not in strict
skipLibCheck
Skip .d.ts checks: faster, hides library errors
verbatimModuleSyntax
Imports kept as written; types need import type

Basic types and inferenceLesson 1 →

  • Primitives are lowercase: string, number, boolean, bigint, symbol, null, undefined. Tuple: [string, number].
  • let x = "a" is string (widened). const x = "a" is "a" (literal). { dir: "up" } has dir: string; fix with an annotation or as const.
  • Let inference work for locals. Write parameter types, exported return types, and empty starts.
TypeMeaningCalls allowed?
anyChecker off, spreads everywhereAnything
unknownSafe unknown: narrow firstNone until narrowed
neverNo value (throws, impossible)n/a

Narrowing and unionsLesson 1 →

type Shape =
  | { kind: "circle"; r: number }
  | { kind: "square"; side: number };

function area(s: Shape): number {
  switch (s.kind) {
    case "circle":
      return Math.PI * s.r ** 2;
    case "square": return s.side ** 2;
    default: return fail(s); // takes never
  }
}
  • Narrow with typeof, instanceof, in, === null, truthiness, Array.isArray, or a kind tag.
  • Traps: typeof null is "object"; if (!count) also drops 0.
  • x! adds nothing at runtime. Use a real check that throws, ??, or ?..

Objects, interface vs typeLesson 2 →

Topicinterfacetype
Declared twiceMergesError TS2300
Unions, tuples, functionsNoYes
Extendextends (clash errors early)& (clash becomes never)
  • ? makes a property optional (adds | undefined). readonly is compile time only; use Object.freeze for real.
  • Index signature { [k: string]: number } reads as number | undefined with noUncheckedIndexedAccess. Known keys: Record<"a" | "b", number>.

Structural typing and excess checksLesson 2 →

  • Shape decides, not the name. Two classes with the same fields are interchangeable unless branded.
  • Excess property check: only for an object literal written in place (TS2561, with a "Did you mean" hint). Not for a variable or a cast.
  • Weak type (TS2559): an all-optional type with no property in common is refused.
const dirs = ["up", "down"] as const;
type Dir = (typeof dirs)[number];
// "up" | "down"

const p = { red: [255, 0], green: "#0f0" }
  satisfies Record<string, string|number[]>;
p.red.map(n => n);   // precise type kept

satisfies checks and keeps precision; an annotation widens; as skips the check.

Classes and enumsLesson 2 →

FeatureBehaviour
privateCompile time only; visible in Object.keys
#pinReal runtime private; hidden from keys
Parameter propertyconstructor(public x): needs codegen, banned by erasableSyntaxOnly
enumRuntime object; prefer a union of literals

Watch out: const { method } = obj; method() loses this and crashes with no tsc error. Use an arrow field, bind, or call obj.method().

Function types and overloadsLesson 3 →

  • Optional and default parameters go last; rest ...xs: number[] collects the rest.
  • void = ignore the result (() => 42 fits () => void). never = never returns.
  • Overloads: several body-less signatures plus one implementation. Callers see only the overloads, tried in order (TS2769 if none fit). Prefer a union or generic.
  • this parameter: function who(this: { name: string }) is erased and stops lost-this calls (TS2684).

GenericsLesson 3 →

function first<T>(xs: T[]): T | undefined {
  return xs[0];
}
function len<T extends { length: number }>(
  x: T) {
  return x.length;      // T kept
}
function get<T, K extends keyof T>(
  o: T, k: K): T[K] {
  return o[k];      // wrong key = error
}
  • Inference reads arguments and callbacks. With nothing to infer (empty list), pass the type argument yourself.
  • A bare type parameter has no members (TS2339); add extends. If it appears once in the signature, drop it.
  • keyof = key union. typeof v = a value's type. Indexed access T[K]. (typeof a)[number] = item type.

Watch out: A generic fetch that only casts T is a lie: make the caller pass a parser. For an event emitter, one map from event name to payload, with K extends keyof E, ties every event to its data.

Utility typesLesson 4 →

TypeResult
Partial / RequiredAll optional / all required
Pick / OmitKeep / drop object keys
RecordObject with given keys and value type
Exclude / ExtractDrop / keep union members
ReturnType, ParametersRead a function type
AwaitedType after await
  • PATCH body: Partial of Omit of the User without id. Async result: Awaited of ReturnType of the function.
  • Omit on a union collapses to the shared keys. Make a distributive helper.

Mapped, conditional, template typesLesson 4 →

type P2<T> = { [K in keyof T]?: T[K] };
type Mut<T> =
  { -readonly [K in keyof T]-?: T[K] };
type Getters<T> = {
  [K in keyof T
    as `get${Capitalize<string & K>}`]:
    () => T[K];
};
type Ret<T> =
  T extends (...a: any[]) => infer R
    ? R : never;

as renames keys (never drops them). [T] wrapping stops distribution.

  • Looping over keyof T keeps modifiers. +/- add or remove readonly and ?.
  • A naked type parameter in extends distributes over a union. Wrap both sides in [ ] to stop it; needed to test for never.
  • Template types build string unions, such as "onClick" | "onFocus". Stop when the type is harder than the code (TS2589 = too deep).

Modules and declaration filesLesson 5 →

ProjectmodulemoduleResolution
Node app or librarynodenextnodenext
Vite or Next.jsesnext / preservebundler
  • Under nodenext write the output extension: import "./util.js" in a .ts file.
  • paths helps only the checker. node still fails with ERR_MODULE_NOT_FOUND; add a bundler alias or imports in package.json.
  • @types/x gives types to JS libraries (TS7016 if missing). A .d.ts with a top-level import/export augments instead of declaring.
  • __dirname passes tsc but crashes in ESM. Use import.meta.dirname.
  • Monorepo: references and tsc -b. Library: declaration, exports with types first, check with @arethetypeswrong/cli.
// env.d.ts
declare global {
  namespace NodeJS {
    interface ProcessEnv {
      DATABASE_URL: string }
  }
}
export {};

React, Express and runtime validationLesson 6 →

CaseWrite
Empty stateGive useState a type argument: User | null, string[]
EventsChangeEvent of the element, or an inline handler
Element refuseRef starts null: use ref.current?.focus()
ReducerAction union with a type tag
ExpressRequest params, body, query; req.body is any

Watch out: A hand-written guard x is Todo is trusted fully; checking only id lets a bad done through. Brand same-shaped ids (string & { __brand: "UserId" }) in one validated place.

const User = z.object({ id: z.number() });
type User = z.infer<typeof User>;

const r = User.safeParse(req.body);
if (!r.success)
  return res.status(400).json(r.error);
const env = Env.parse(process.env);

parse throws; safeParse returns { success, data or error }. Parse process.env once at startup; use z.coerce.number().

Common errors and fixesLesson 7 →

CodeMeansFix
TS2322Type A not assignable to BFix the value or widen the type
TS2339Property does not existFix the name or the type
TS2345Wrong argument typePass the right type
TS18048 / TS2532Possibly undefinedif, ?., ??
TS2307Cannot find modulePath, extension, install, declare module
  • Read errors from the bottom: the last indented line is the cause. tsc shows the first difference only.
  • Use @ts-expect-error with a reason, not @ts-ignore: a stale one fails with TS2578.

Migrating to strictLesson 7 →

  1. Add allowJs and a tsconfig. Keep the build green.
  2. Add checkJs (or // @ts-check per file) with JSDoc types.
  3. Rename to .ts a folder at a time, still non-strict.
  4. Turn on noImplicitAny first (any hides other errors), then strictNullChecks.
  5. Then the other strict flags one by one; noUncheckedIndexedAccess last.
  6. Commit after each step: all at once gave 18 errors, in steps 10, then 1 at a time.
  • CI: tsc --noEmit (or tsc -b). Plain tsc exits 2 but still writes JS.
  • typescript-eslint adds no-floating-promises, no explicit any, no !, no @ts-ignore.

Interview one-linersLesson 0 →

QuestionShort answer
any vs unknown?any switches checks off; unknown forces a narrow first.
interface vs type?interface merges; type names unions, tuples, functions.
Enum or union?Union of literals: no runtime code. as const object if you need the list.
Why can types not protect API data?They are erased; validate at the edge with a schema.
When is as fine?A check the compiler cannot follow, inside a tested helper. Never on outside data.

A cheat sheet is a reminder, not a teacher. Each card links to the lesson that explains it step by step.