TypeScript interview questions
The TypeScript questions interviewers actually ask: how the type system thinks, how to narrow and constrain types, and where compile-time safety stops.
Answer out loud or on paper before revealing. If your answer misses a key point, revisit the linked guide or explainer.
What is the difference between an `interface` and a `type` alias? When do you use each?
Reveal model answer
Both can describe object shapes and are mostly interchangeable. Interfaces can be re-opened and merged by declaring them again, and extend with `extends`; type aliases cannot be re-opened but can name any type, including unions, tuples, primitives, mapped and conditional types, and combine with `&`. A common rule is interfaces for public object contracts that consumers may augment, and type aliases for unions and computed types; consistency within a codebase matters more than the choice.
WHAT A STRONG ANSWER COVERS
- Declaration merging is interface-only (used to augment library types such as Window).
- Only type aliases can express unions, tuples, mapped and conditional types directly.
- `extends` on interfaces reports conflicts and is often cheaper for the compiler than large intersections.
- Both are erased at compile time.
RED FLAGS
- Claiming interfaces exist at runtime.
- Claiming types cannot describe objects or be extended at all.
LIKELY FOLLOW-UPS
- How would you add a property to a third-party library's interface?
- What happens when two intersected types have the same property with incompatible types?
SOURCES
Explain `any`, `unknown` and `never`. Which should a function parsing untrusted JSON return?
Reveal model answer
`any` switches checking off: anything can be assigned to it and you can do anything with it, and it spreads silently. `unknown` also accepts any value, but you cannot use it until you narrow it, so it is the safe top type and the right return type for parsed JSON. `never` is the empty type: it has no values, is the return type of functions that always throw or never return, and is what remains after a union has been exhaustively narrowed.
WHAT A STRONG ANSWER COVERS
- `any` disables type checking and is contagious.
- `unknown` forces narrowing or validation before use.
- `never` is the bottom type, assignable to everything, with nothing assignable to it.
- `never` powers exhaustiveness checks and filtering in conditional types.
RED FLAGS
- Treating `unknown` and `any` as synonyms.
- Using `as SomeType` on parsed JSON and calling it typed.
LIKELY FOLLOW-UPS
- How does `noImplicitAny` change things?
- Why does `Exclude<'a' | 'b', 'a'>` work in terms of never?
SOURCES
TypeScript is structurally typed. What does that mean, and why does `const p: Point = { x: 1, y: 2, z: 3 }` error while passing a variable holding the same object does not?
Reveal model answer
Compatibility is decided by shape, not by declared name: any value with at least the required members is assignable, even if it was never declared as that type. The object literal case errors because of excess property checking, an extra rule for fresh object literals that catches typos. Once the object is stored in a variable it is no longer fresh, so the extra `z` is allowed by normal structural rules.
WHAT A STRONG ANSWER COVERS
- Assignability is based on members, not names or declarations.
- Two unrelated classes with the same shape are interchangeable.
- Excess property checks only apply to fresh object literals.
- For nominal-like safety (UserId vs OrderId) use branded types.
RED FLAGS
- Expecting a class to be required for assignability, as in Java or C#.
- Believing extra properties are always rejected.
LIKELY FOLLOW-UPS
- How would you implement a branded `UserId` type?
- How do private class fields affect structural compatibility?
SOURCES
What is wrong with `function getProp<T>(obj: T, key: string) { return obj[key]; }`, and how do you type it properly?
Reveal model answer
It fails to compile because `string` cannot index an arbitrary `T`, and even with a cast the return type would be meaningless. Constrain the key with a second type parameter: `function getProp<T, K extends keyof T>(obj: T, key: K): T[K]`. Now only real keys are accepted and the return type is the indexed access type of that specific key. Constraints with `extends` say what a type parameter must at least have, without losing the caller's specific type.
WHAT A STRONG ANSWER COVERS
- `extends` on a type parameter is a constraint, not inheritance.
- `keyof T` and indexed access `T[K]` relate parameters to each other.
- Generics preserve the caller's specific type instead of widening to a base type.
- A generic used only once in a signature is usually unnecessary.
RED FLAGS
- Fixing it with `any` or `as any`.
- Using generics everywhere where a plain parameter type would do.
LIKELY FOLLOW-UPS
- What does `NoInfer` do?
- How would you type a `pick(obj, keys)` helper?
SOURCES
How does narrowing work? Name the ways to narrow a union and explain a user-defined type guard.
Reveal model answer
The compiler follows control flow and refines a variable's type in each branch based on checks it understands: `typeof`, `instanceof`, `in`, equality and truthiness checks, discriminant property checks, and assignments. A user-defined type guard is a function returning `value is Type`; when it returns true, the caller's variable is narrowed. The guard is trusted, not verified, so a wrong implementation silently lies to the compiler. Assertion functions (`asserts value is Type`) narrow by throwing on failure.
WHAT A STRONG ANSWER COVERS
- Control-flow analysis narrows per branch.
- Built-in guards: typeof, instanceof, in, equality, truthiness, discriminants.
- `x is T` predicates and `asserts x is T` functions extend narrowing.
- The compiler trusts a predicate's implementation; bugs in it become type holes.
- Truthiness narrowing also removes 0 and empty string, a classic bug.
RED FLAGS
- Using `as` casts instead of narrowing.
- Not realising a type predicate can be wrong.
LIKELY FOLLOW-UPS
- Why can narrowing be lost inside a callback?
- How does `Array.prototype.filter` work with a type predicate?
SOURCES
What is a discriminated union, and how do you make the compiler tell you when a new variant is not handled?
Reveal model answer
A union of object types that share a literal-typed property such as `kind` or `status`; checking that property narrows to exactly one member, so each branch sees only valid fields. For exhaustiveness, add a default branch that assigns the value to a `never` variable or passes it to `assertNever(x: never)`. When someone adds a variant and forgets a case, the leftover type is no longer `never` and the build fails.
WHAT A STRONG ANSWER COVERS
- A shared literal discriminant makes narrowing precise.
- Model states so impossible combinations cannot be represented (no `loading: true` with `data`).
- Exhaustiveness via `never` turns a missed case into a compile error.
- A runtime throw in the default branch also protects against unexpected data.
RED FLAGS
- Modelling state as many optional booleans and fields.
- Using a default branch that silently returns, hiding new variants.
LIKELY FOLLOW-UPS
- How would you model a request state: idle, loading, success, error?
- Does `switch(true)` narrowing work?
SOURCES
Which built-in utility types do you use most, and what do `Partial`, `Pick`, `Omit`, `Record` and `ReturnType` do?
Reveal model answer
`Partial<T>` makes every property optional, which suits patch payloads. `Pick<T, K>` and `Omit<T, K>` select or remove properties to derive DTOs from a model. `Record<K, V>` builds an object type with keys K and values V. `ReturnType<F>` extracts a function's return type, often together with `Awaited` for async functions. They are all built from mapped and conditional types, so you can write your own when needed.
WHAT A STRONG ANSWER COVERS
- Partial, Required and Readonly change modifiers on every property.
- Pick and Omit derive types from a single source of truth.
- Record describes dictionaries; with a union key it forces all keys present.
- ReturnType, Parameters and Awaited infer from functions and promises.
- Omit does not check that the omitted keys exist.
RED FLAGS
- Duplicating near-identical interfaces by hand instead of deriving them.
- Thinking Readonly makes objects immutable at runtime.
LIKELY FOLLOW-UPS
- Implement `Partial` yourself with a mapped type.
- Why is `Record<string, T>` indexing unsafe without `noUncheckedIndexedAccess`?
SOURCES
What does the `satisfies` operator do, and how is it different from a type annotation or an `as` assertion?
Reveal model answer
`expr satisfies T` checks that the expression is assignable to T but keeps the expression's own inferred type. An annotation `const x: T = ...` also checks but widens the variable to T, losing literal keys and per-property types. `as T` does not check compatibility properly at all; it tells the compiler to trust you. `satisfies` is ideal for config objects and lookup tables where you want validation plus precise inference.
WHAT A STRONG ANSWER COVERS
- satisfies validates without widening.
- Annotations validate and widen to the declared type.
- `as` is an unchecked (or loosely checked) assertion that can hide bugs.
- Combining `as const satisfies T` gives readonly literals that are still checked.
RED FLAGS
- Thinking satisfies changes runtime behaviour.
- Using `as` to make errors go away.
LIKELY FOLLOW-UPS
- Why might `Record<Colors, string>` with satisfies catch a typo an annotation also catches, but keep more information?
SOURCES
Enums or string literal unions? What are the trade-offs, and how does Node.js type stripping affect the choice?
Reveal model answer
Enums are one of the few TypeScript features that emit runtime code: an object (with reverse mappings for numeric enums), and `const enum` adds inlining pitfalls across packages and with isolatedModules. A union of string literals, optionally backed by an `as const` object for runtime values, gives the same safety, is plain JavaScript and works with structural typing. Node.js runs TypeScript by stripping types and rejects enums with ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX, and TypeScript 5.8's `--erasableSyntaxOnly` flags enums, so unions are the more portable default.
WHAT A STRONG ANSWER COVERS
- Enums generate runtime objects; unions erase completely.
- Numeric enums accept arbitrary numbers in some cases and have reverse mappings.
- `const enum` breaks with isolatedModules and across library versions.
- `as const` object plus `typeof Obj[keyof typeof Obj]` gives runtime values and a union type.
- Erasable-only setups (Node type stripping, erasableSyntaxOnly) disallow enums.
RED FLAGS
- Not knowing enums exist at runtime.
- Assuming all TypeScript syntax is just erased.
LIKELY FOLLOW-UPS
- What other syntax is not erasable (parameter properties, namespaces)?
- How would you iterate over all allowed values of a union?
SOURCES
What are `.d.ts` declaration files, and what do you do when a JavaScript dependency has no types?
Reveal model answer
Declaration files contain only type information describing the shape of JavaScript code; they produce no output and are how libraries ship types (generated with `declaration: true`) or how DefinitelyTyped provides `@types/*` packages. If a package has no types, first check for bundled types or `@types/pkg`; otherwise write a minimal `declare module "pkg"` declaration covering what you use, in a file included by tsconfig. Declarations are promises, not checks: if they are wrong, the compiler will believe them.
WHAT A STRONG ANSWER COVERS
- `.d.ts` files describe types only and emit no JavaScript.
- Libraries ship them via the `types` or `exports` conditions in package.json.
- `@types/*` from DefinitelyTyped covers untyped packages.
- `declare module` and `declare global` add or augment declarations.
- Hand-written declarations can drift from the real runtime API.
RED FLAGS
- Silencing with `// @ts-ignore` on every import.
- Believing declaration files validate the library at runtime.
LIKELY FOLLOW-UPS
- How do you augment a global like `Window` or an Express `Request`?
- What does `skipLibCheck` skip and why is it common?
SOURCES
A teammate writes `const user = (await res.json()) as User;`. What is the problem, and how should data crossing a trust boundary be handled?
Reveal model answer
Types are erased at compile time, so the assertion checks nothing: if the API returns a different shape, the code proceeds with a lie and fails far from the cause. Treat external input (HTTP responses and requests, env vars, localStorage, queue messages, database JSON) as `unknown` and validate it at the boundary with a schema library such as zod or valibot, or JSON Schema with Ajv. Derive the static type from the schema (`z.infer`) so validation and types cannot drift, and decide explicitly how to handle failures.
WHAT A STRONG ANSWER COVERS
- TypeScript has no runtime presence; `as` and generics on fetch wrappers are trust, not checks.
- Validate at trust boundaries, then rely on types internally.
- Single source of truth: infer types from the runtime schema.
- Decide on failure behaviour: reject, log and degrade, or strip unknown keys.
RED FLAGS
- Saying TypeScript validates API responses.
- Validating deep inside business logic instead of at the edge.
- Writing a type and a separate hand-rolled validator that can drift.
LIKELY FOLLOW-UPS
- What does validation cost at high throughput, and how would you limit it?
- How do you share schemas between client and server?
SOURCES