R3·TS · Part 2 · The technical rounds
TypeScript, properly
If your resume says 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.
- product
- saas
- agency
- service (less often)
Everything open, top to bottom.
09On this page
- interface vs type.
- Write a generic function you have actually needed.
- Write Partial, Pick, Omit, Readonly from scratch.
- any vs unknown vs never.
- What is a discriminated union and why do you care?
- Write a type guard and explain x is T.
- Explain satisfies and when it beats an annotation.
- What is structural typing, and how does it differ from nominal typing?
- Rapid-fire TypeScript
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"A type alias cannot be extended." It can, with an intersection — type B = A & { extra: string }. The real differences are declaration merging and what each can express; inventing others is what makes this answer sound memorised.
- Which would you use for a React component's props?
- Can a type extend an interface? (Yes, via intersection.)
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>
}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
}Presenting apiGet<User>(url) as type-safe. The generic is an assertion about data the compiler never saw — if the API changes shape, the build stays green and the crash happens in production, three calls away from the fetch.
- 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?
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 }- What is a conditional type? Write one.
- What does infer do?
any vs unknown vs never.
What they are really testingWhether your codebases are actually type-safe or just annotated.
anyswitches the checker off for that value and is contagious — anything derived from it is also unchecked. It is the escape hatch, and everyanyis a small hole in the guarantee.unknownis 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.neveris the empty type — the return type of a function that always throws, and the tool for exhaustiveness checking.
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}`)
}
}try { risky() }
catch (e) {
// e is unknown under useUnknownInCatchVariables — anything can be thrown
if (e instanceof Error) log(e.message)
else log(String(e))
}"unknown is basically any." They are opposites at the point of use: any lets you do anything without checking, unknown lets you do nothing until you have checked. That difference is the entire reason unknown exists.
- Why is `catch (e: any)` dangerous?
- When is `any` genuinely the right answer? (Migrating a large JS codebase incrementally.)
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.
type Bad<T> = {
loading: boolean
data?: T
error?: string
}
// loading:true + data + error — meaningless, but legaltype Result<T> =
| { status: 'loading' }
| { status: 'ok'; data: T }
| { status: 'error'; error: string }
if (r.status === 'ok') r.data // narrowed — data exists here and nowhere elseSay the phrase "make impossible states unrepresentable" — it is the actual design principle and interviewers who know it will notice you know it.
- How does the compiler narrow this?
- What is a type guard? Write one with the is keyword.
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- How does that differ from a cast?
- Where do you validate — the guard or a schema library?
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 catchTreating satisfies as another spelling of as. as tells the compiler to trust you and silences the error; satisfies asks the compiler to check you and keeps it. Writing as Config where you meant satisfies Config hides exactly the typo you wanted caught.
- Where would you use it in a theme or route table?
- What is `as const` and how does it interact?
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 errorAlso 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.
- Why does assigning a literal error but a variable not?
- What is a branded type used for in practice?
Rapid-fire TypeScript
What they are really testingBreadth across the type system.
keyof,typeof, indexed access.keyof Tis the union of keys;typeof xlifts a value into a type;T['id']reads a property's type.- Conditional types.
T extends U ? A : B. Withinferyou 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
Excludeworks. 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 enumhas its own inlining problems. - Declaration files.
.d.tsdescribes 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(makesarr[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
strictFunctionTypesexists. - 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.
Finished this round?
0 of 27 rounds read in this book