TypeScript, properly
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.
interface vs type.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- Which would you use for a React component's props?
- Can a type extend an interface? (Yes, via intersection.)
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
}- 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?
Partial, Pick, Omit, Readonly from scratch.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.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))
}- Why is `catch (e: any)` dangerous?
- When is `any` genuinely the right answer? (Migrating a large JS codebase incrementally.)
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.
x is T.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?
satisfies and when it beats an annotation.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- Where would you use it in a theme or route table?
- What is `as const` and how does it interact?
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?
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.