The fundamentals interviewers test are the ones that prevent bugs: knowing that TypeScript is structural, that any disables checking for everything it touches, how narrowing works and where it fails, and why satisfies exists. This page is the checklist.
Point; extra properties are allowed except in fresh object literals (excess property checks). Nominal behaviour needs branded types (type UserId = string & { __brand: 'UserId' }).any versus unknown versus never: any opts out of checking and spreads through every expression it touches; unknown is the safe top type that must be narrowed before use (the right type for JSON.parse, catch (e), external data); never is the empty type: functions that throw, unreachable branches, and the tool for exhaustiveness checks.A | B is either; A & B is both (conflicting property types intersect to never). Discriminated unions share a literal kind/type/status property and are the idiom for state machines, API results and component variants.as const: 'sm' | 'md' | 'lg' instead of string; as const freezes a value to its literal, readonly type so typeof SIZES[number] derives the union from the array. Template literal types build strings from unions (`${Size}-${Color}`).typeof, instanceof, in, equality, truthiness, discriminant checks, Array.isArray, user-defined type guards ((x): x is Foo), assertion functions (asserts x is Foo), and since TS 5.5 inferred type predicates from simple filter callbacks. Control flow analysis narrows let variables but not properties across function calls, and not inside callbacks executed later.satisfies (4.9) checks a value against a type without widening it: const routes = {...} satisfies Record<string, Route> keeps the literal keys for autocomplete while validating the shape. Use it for config objects and lookup tables instead of a type annotation.readonly, tuples, const type parameters: readonly T[]/ReadonlyArray, Readonly<T>, tuple types with optional and rest elements, labelled tuples for defineEmits, const T type parameters (5.0) to infer literals in generic calls.enum emits runtime code, has surprising numeric semantics and cannot be erased by Node's type stripping; string literal unions plus an as const object give the same ergonomics with zero runtime and full inference. erasableSyntaxOnly (5.8) forbids enums, namespaces and parameter properties so .ts runs natively in Node 24.undefined: prop?: T means "may be absent"; with exactOptionalPropertyTypes you cannot assign undefined explicitly. noUncheckedIndexedAccess makes arr[i] and record[key] T | undefined, which catches the most common runtime crash class.strict bundles strictNullChecks, noImplicitAny, strictFunctionTypes, strictPropertyInitialization, useUnknownInCatchVariables and more; add noUncheckedIndexedAccess, noImplicitOverride, noFallthroughCasesInSwitch. TypeScript 7 defaults strict to on.// Illegal states unrepresentable: a discriminated union instead of { loading, error, data } booleans.
export type OrderState =
| { status: 'idle' }
| { status: 'loading', startedAt: number }
| { status: 'success', order: Order }
| { status: 'error', error: { status: number, message: string } }
export const ORDER_STATUSES = ['draft', 'paid', 'shipped', 'cancelled'] as const
export type OrderStatus = typeof ORDER_STATUSES[number] // 'draft' | 'paid' | 'shipped' | 'cancelled'
// Branded ids: an OrderId cannot be passed where a UserId is expected, although both are strings at runtime.
export type OrderId = string & { readonly __brand: 'OrderId' }
export const asOrderId = (raw: string): OrderId => raw as OrderId // the one sanctioned cast, at the boundary
// Exhaustiveness: adding a status without handling it fails to compile.
export function describe(state: OrderState): string {
switch (state.status) {
case 'idle': return 'Nothing loaded'
case 'loading': return `Loading since ${state.startedAt}`
case 'success': return `Order ${state.order.id}`
case 'error': return `Failed with ${state.error.status}`
default: {
const exhaustive: never = state
return exhaustive
}
}
}
Narrowing that works and narrowing that does not:
function isOrder(value: unknown): value is Order {
return typeof value === 'object' && value !== null && 'id' in value && 'status' in value
}
const raw: unknown = JSON.parse(text)
if (isOrder(raw)) console.log(raw.status) // narrowed
const maybe: string | undefined = getName()
if (maybe) {
setTimeout(() => maybe.toUpperCase(), 0) // ok: `const` cannot change
}
const box = { value: undefined as string | undefined }
if (box.value) {
refresh() // could reassign box.value
box.value.toUpperCase() // TS still narrows here, but it is unsound after a call: copy to a const first
}
// satisfies keeps literal keys while checking the shape
const routes = {
home: { path: '/', auth: false },
account: { path: '/account', auth: true },
} satisfies Record<string, { path: string, auth: boolean }>
routes.account.path // autocomplete for `home` | `account`, unlike an annotation which would widen to string keys
const user = data as User tells the compiler to trust you; the runtime does nothing. Casting API data, event.target, or JSON.parse output is where TypeScript's guarantees end. Use a type guard, a schema, or satisfies when the value is a literal you control.
A variable narrowed with if (x) is not narrowed inside a callback that runs later if it is a let or a property, because it might change before the callback runs. Copy to a const inside the check.
loading, error, data) as a discriminated union and count the impossible combinations that disappear.noUncheckedIndexedAccess in a small project and fix the errors; note how many were real potential crashes.enum with an as const object plus a union type and check that Node runs the file directly with type stripping."any turns off type checking for everything the value flows into, so a single any from a fetch can silently untype a whole module; I avoid it except at deliberately unchecked seams. unknown is the honest top type: I can hold anything but must narrow with a guard or a schema before using it, which is exactly right for JSON, catch clauses and external inputs. never is the empty type: the return type of functions that throw, the type of an exhausted union in a switch default, which is how I make the compiler fail when someone adds a variant and forgets to handle it."
"A literal union such as 'sm' | 'md' | 'lg' costs nothing at runtime, infers from an as const array so the values exist once, erases cleanly for Node's native type stripping, and reads as documentation at every call site; enums emit code and have numeric quirks. satisfies solves a different problem: annotating a config object as Record<string, Route> widens the keys and loses autocomplete, while satisfies validates the shape and keeps the literal type, so routes.account is still known to exist. Both are about keeping information the compiler already has."
Nothing yet. Notes filed here show up on this page and the section overview.
TypeScript in practice
How senior interviews test TypeScript — as a design tool rather than a syntax quiz — and how the pages on the type system, generics, Vue and Nuxt typing, configuration and TypeScript 7, runtime validation and typing smells fit together.
Generics and inference
Generic functions and constraints, inference sites and NoInfer, conditional and distributive types, infer, mapped types with key remapping, template literal types, the utility types worth knowing, and when a generic is over-engineering.