Skip to the notes
JSGroundwork
JSGroundwork handwritten · web dev
✎Playground→⌘Problems↻Review🔥Progress

Chapters

20 chapters
⌕
Beginner15›
00Scouting reportR1Screening callR2Machine codingR3JavaScript & TSR3·TSTypeScriptR4React & Next.jsR5Node & NestJSR6Databases & RedisR7DSA roundR8System designR9AWS, Docker, CI/CDR10Resume grillingR11BehaviouralR12HR & the number✓The week before
Advanced5›
50LWhat changes at ₹50L50LHard DSA50LDistributed systems50LRuntime internals50LStaff behavioural
/ search[ ] chaptert top

Ready to read

JSJavaScript⑂Git◎Interview prepΣDSA in JSSDSystem Design
More topics15›
</>HTML{ }CSS⚛ReactNNext.jsNeNest.jsTSTypeScriptNoNode.js🐳DockerDBSQL & Databases✓Testing🔒Web Security☁Cloud & DevOps◈GraphQL◆Redis☸Kubernetes
100%
R3·TS

TypeScript, properly

LengthFolded into R3
WhoSenior engineer
DecidesWhether "TypeScript" means typed JS or types
Fail modeKnowing the syntax, not the type system
productsaasagencyservice

Your resume says six years of TypeScript across both ends. That raises the bar rather than lowering it: they will not ask what an interface is, they will ask you to write a mapped type.

3TS.1
interface vs type.
What they are really testingWhether you have a practical rule or a memorised list of differences.

interface supports declaration merging and extends, and is the conventional choice for object shapes and public API contracts a consumer might augment. type can express everything an interface cannot: unions, intersections, tuples, mapped types, conditional types, and aliases of primitives.

The practical rule, which is the answer they want: interface for object shapes you may extend or that a library consumer may augment; type for everything else. Do not invent a deep philosophical difference — interviewers respect the practical answer.

// declaration merging — only interfaces do this
interface Window { myApp: App }        // augments the global Window

// unions — only types do this
type Status = 'idle' | 'loading' | 'done'
type Id = string | number
They will push with
  • Which would you use for a React component's props?
  • Can a type extend an interface? (Yes, via intersection.)
3TS.2
Write a generic function you have actually needed.
What they are really testingWhether generics are a tool you use or a chapter you read.

Then deliver the honest caveat, which is the actual senior answer: this is a lie to the compiler. res.json() returns whatever the server sent; the generic asserts a shape nobody verified. If the contract matters, parse it and infer the type from the schema so the runtime check and the compile-time type cannot drift apart:

async function apiGet<T>(url: string): Promise<T> {
  const res = await fetch(url)
  if (!res.ok) throw new ApiError(res.status, url)
  return res.json() as Promise<T>
}
one source of truth for the shape
import { z } from 'zod'

const User = z.object({ id: z.string(), email: z.string().email() })
type User = z.infer<typeof User>      // derived, never hand-written

async function apiGet<S extends z.ZodTypeAny>(url: string, schema: S): Promise<z.infer<S>> {
  const res = await fetch(url)
  return schema.parse(await res.json())   // throws on a contract break, loudly
}
They will push with
  • What does `extends` mean in a generic constraint?
  • How would you type a function that takes a key of an object and returns that property's type?
3TS.3
Write Partial, Pick, Omit, Readonly from scratch.
What they are really testingMapped types. Naming the utilities is worth nothing; writing them is the question.

The -? modifier and key remapping with as are the two details that mark you as someone who writes types rather than consumes them.

type MyPartial<T>  = { [K in keyof T]?: T[K] }
type MyRequired<T> = { [K in keyof T]-?: T[K] }        // -? strips optionality
type MyReadonly<T> = { readonly [K in keyof T]: T[K] }

type MyPick<T, K extends keyof T> = { [P in K]: T[P] }
type MyOmit<T, K extends keyof T> = MyPick<T, Exclude<keyof T, K>>

type MyRecord<K extends keyof any, V> = { [P in K]: V }

// key remapping (TS 4.1+) — rename while mapping
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
}
// Getters<{ name: string }>  →  { getName: () => string }
They will push with
  • What is a conditional type? Write one.
  • What does infer do?
3TS.4
any vs unknown vs never.
What they are really testingWhether your codebases are actually type-safe or just annotated.
  • any switches the checker off for that value and is contagious — anything derived from it is also unchecked. It is the escape hatch, and every any is a small hole in the guarantee.
  • unknown is the safe top type: you can hold anything, but you must narrow before you use it. It is the correct type for JSON you just parsed, for a caught error, and for anything crossing a trust boundary.
  • never is the empty type — the return type of a function that always throws, and the tool for exhaustiveness checking.
the exhaustiveness check — the most useful never in practice
type Status = 'idle' | 'loading' | 'done'

function label(s: Status) {
  switch (s) {
    case 'idle':    return 'Ready'
    case 'loading': return 'Working'
    case 'done':    return 'Finished'
    default:
      const _exhaustive: never = s     // compile error the day someone adds 'error'
      throw new Error(`unhandled: ${s}`)
  }
}
catch is unknown, not Error
try { risky() }
catch (e) {
  // e is unknown under useUnknownInCatchVariables — anything can be thrown
  if (e instanceof Error) log(e.message)
  else log(String(e))
}
They will push with
  • Why is `catch (e: any)` dangerous?
  • When is `any` genuinely the right answer? (Migrating a large JS codebase incrementally.)
3TS.5
What is a discriminated union and why do you care?
What they are really testingWhether you make impossible states unrepresentable, or model everything as optional fields.

A union of object types sharing a literal field the compiler can narrow on. It is the single most valuable pattern in application TypeScript.

sixteen possible states, most of them nonsense
type Bad<T> = {
  loading: boolean
  data?: T
  error?: string
}
// loading:true + data + error  — meaningless, but legal
three states, all of them real
type Result<T> =
  | { status: 'loading' }
  | { status: 'ok';    data: T }
  | { status: 'error'; error: string }

if (r.status === 'ok') r.data      // narrowed — data exists here and nowhere else

Say the phrase "make impossible states unrepresentable" — it is the actual design principle and interviewers who know it will notice you know it.

They will push with
  • How does the compiler narrow this?
  • What is a type guard? Write one with the is keyword.
3TS.6
Write a type guard and explain x is T.
What they are really testingNarrowing beyond typeof and instanceof.

The danger to name out loud: a predicate is an assertion you are making, not a check the compiler performs. If isCat returns true for a dog, TypeScript believes it and you get a runtime crash with a green build. That is why schema validation at real boundaries beats hand-written guards.

type Cat = { kind: 'cat'; meow(): void }
type Dog = { kind: 'dog'; bark(): void }

// the return type 'pet is Cat' teaches the compiler, it does not check anything
function isCat(pet: Cat | Dog): pet is Cat {
  return pet.kind === 'cat'
}

// assertion function — narrows for the rest of the scope
function assertDefined<T>(v: T, msg = 'missing'): asserts v is NonNullable<T> {
  if (v == null) throw new Error(msg)
}

assertDefined(user)
user.email      // narrowed from User | null to User, no cast
They will push with
  • How does that differ from a cast?
  • Where do you validate — the guard or a schema library?
3TS.7
Explain satisfies and when it beats an annotation.
What they are really testingWhether you have kept up with the language past 4.x.

An annotation widens the value to the declared type; satisfies checks the value against the type while keeping the narrow inferred type. You want it whenever you need both the check and the literal precision.

type Config = Record<string, string | number>

const a: Config = { port: 3000, host: 'localhost' }
a.port                       // string | number — precision lost

const b = { port: 3000, host: 'localhost' } satisfies Config
b.port                       // number — checked AND narrow
// and a typo in a key is still an error, which a bare const would not catch
They will push with
  • Where would you use it in a theme or route table?
  • What is `as const` and how does it interact?
3TS.8
What is structural typing, and how does it differ from nominal typing?
What they are really testingThe mental model that explains half of TypeScript's surprises.

TypeScript compares types by shape, not by name. If an object has the required members it is assignable, regardless of what it was declared as. Java and C# are nominal — a name must match.

The practical consequence people get bitten by: two different domain ids are the same type.

type UserId = string
type OrderId = string
function get(id: UserId) {}
get(orderId)                 // compiles. Same shape. Real bug.

// branded types restore nominal behaviour
type UserId  = string & { readonly __brand: 'UserId' }
type OrderId = string & { readonly __brand: 'OrderId' }
get(orderId)                 // now an error

Also worth naming: excess property checking applies only to object literals assigned directly. Assign the literal to a variable first and the check disappears — which is why a stray key sometimes errors and sometimes does not.

They will push with
  • Why does assigning a literal error but a variable not?
  • What is a branded type used for in practice?
3TS.9
Rapid-fire TypeScript
What they are really testingBreadth across the type system.
  • keyof, typeof, indexed access. keyof T is the union of keys; typeof x lifts a value into a type; T['id'] reads a property's type.
  • Conditional types. T extends U ? A : B. With infer you can pull a type out: type Unwrap<T> = T extends Promise<infer U> ? U : T.
  • Distributive conditionals. A conditional over a naked type parameter distributes across a union — which is how Exclude works.
  • as const. Freezes literals and makes arrays readonly tuples. The way to derive a union from a runtime list.
  • Enum vs union of literals. Prefer the union: no runtime object emitted, no reverse-mapping oddities, better narrowing. const enum has its own inlining problems.
  • Declaration files. .d.ts describes the shape of untyped JS. You write one to type a library that ships none, or to declare globals.
  • Strict mode flags that matter. strictNullChecks (the important one), noUncheckedIndexedAccess (makes arr[0] possibly undefined — painful and correct), exactOptionalPropertyTypes.
  • Generics with defaults and constraints. <T extends object = {}>.
  • Variance. Function parameters are checked bivariantly for methods and contravariantly for standalone function types — which is why strictFunctionTypes exists.
  • Utility types worth knowing by name. ReturnType, Parameters, Awaited, NonNullable, Extract, Exclude.
  • Does TypeScript exist at runtime? No. Types are erased at compile time — which is exactly why you still need runtime validation at every boundary.
←previousJavaScript & TS↑ CovernextReact & Next.js→