TypeScript interview questions with model answers

69 questions written alongside the free TypeScript course, each with the answer an interviewer is listening for. 30 are below, spread from first principles to advanced; the rest are in the course.

What TypeScript interviews test

TypeScript interviews focus on the type system: inference and annotations, unions and narrowing, generics, structural typing, utility and mapped types, and what is erased at compile time versus checked at runtime.

  1. 1. What does "types are erased" mean, and what is one practical consequence?

    All type syntax — annotations, interfaces, aliases, generics — is removed at compile time; the emitted JavaScript carries no trace and pays no runtime cost. A practical consequence: you cannot check a TypeScript type at runtime (no instanceof-an-interface), so validating external data like a JSON response needs real runtime checks; the types only prove what the compiler could see.

    From the lesson TypeScript in One Idea: JavaScript Plus Proof

  2. 2. When must you annotate, and when should you not?

    Must: function parameters (never inferable; implicit any under strict is an error), variables declared without initializers, and empty arrays or objects that start without their contents. Should not: initialized locals where the right-hand side makes the type obvious — let n = 0 is already a number, and the annotation adds noise without safety. Return types are judgement: annotate them on exported or public functions to pin the contract.

    From the lesson Type Annotations and Type Inference

  3. 3. When is a tuple the right type, and when does it stop being right?

    A tuple fits fixed-length, position-meaningful data — a coordinate pair, a [value, error] return, a parsed row — especially combined with destructuring. It stops being right when readers must remember what position 2 means: at that point an object with named fields carries the same data self-documentingly. The practical rule: tuples for two or three elements that travel together briefly; named fields for anything that persists.

    From the lesson Typed Arrays and Tuples

  4. 4. What is a discriminated union and why is the never-default trick valuable?

    A union of object types sharing one literal-typed field — the discriminant — so a switch on that field narrows each case to its full variant. Assigning the value to never in the default documents that every case is handled, and when someone adds a new variant, that assignment stops compiling in every switch that has not handled it: the type system turns "we forgot a case" from a production bug into a build failure with a file and line number.

    From the lesson Union Types, Literal Types and Narrowing

  5. 5. What do generics give you that any does not?

    any accepts everything by giving up: the result of an any-typed operation is unchecked, and errors surface at runtime. A generic accepts everything by abstracting: T is unknown inside the function (so the body can only do what it proved via constraints) but bound to the real type at each call, so callers keep full checking on the result. Same flexibility, opposite safety — generics are how a library stays reusable without punching holes in its users’ proofs.

    From the lesson Generic Functions, Types and Constraints

  6. 6. What actually happens at runtime for value as User, and what follows from that?

    Nothing — as is purely a compile-time instruction to treat the expression as the asserted type; no conversion, no validation, no emitted code. It follows that an assertion can be wrong silently: the program compiles, then fails wherever the claimed shape first matters. So assertions belong at boundaries where you have out-of-band knowledge, and anything security- or correctness-critical deserves a runtime check — a type guard or schema validator — instead of a claim.

    From the lesson Type Assertions, Non-null and satisfies

  7. 7. How do you make sure a new variant of a union does not silently slip past existing code?

    I end every exhaustive switch with a default branch that assigns the narrowed value to a variable of type never. When all cases are handled, the value in that branch is never and it compiles; the moment a variant is added to the union, the value reaching the default is that variant and the assignment fails with a message naming it. That turns a missed case into a build error in every file that switches on the union, rather than an undefined return in production. I prefer it to a runtime throw because it costs nothing at runtime and it is found before the deploy. The prerequisite is that the union is actually discriminated — one shared field with literal types — and that the value has not been widened, which is the usual reason the narrowing appears not to work.

    From the lesson Discriminated Unions: Making Impossible States Impossible

  8. 8. Why does Map.get return V | undefined, and what is wrong with using has() to avoid the check?

    Because presence is a runtime fact, not a type-level one — the compiler cannot know which keys a Map holds, so the honest return type includes undefined, and under strictNullChecks that forces a check exactly where the missing-key bug lives. has() does not help, because it returns a boolean about a key rather than a narrowing about a value: the checker cannot connect scores.has("ada") to a separate scores.get("ada") call, and it is right not to, since a delete could sit between them. The correct pattern is one lookup into a const, then a check on that const, which narrows the value and halves the lookups. It is also why I annotate empty collections — new Map() without a type argument infers Map<any, any> and silently turns off checking for everything that passes through it.

    From the lesson Map, Set, Iterables and the lib You Compile Against

  9. 9. Two awaits in a row, both succeeding, correct output. Why might it still be a defect?

    Because sequential awaits serialise work that may be independent. If the second call does not consume the first call's result, the endpoint pays both latencies end to end rather than the larger of the two — a hundred milliseconds becomes two hundred, and under load the effect compounds. Nothing in the type system mentions it: both versions have identical types and identical output, so it survives review unless someone is looking for it. The fix is Promise.all when both must succeed, which also gives back a typed tuple so destructuring keeps each type, or allSettled when a partial result is still useful. The caveat is unbounded parallelism: Promise.all over a large array opens every request at once, so for big fan-outs I batch or use a small pool, and I pass an AbortSignal so a cancelled caller does not leave the work running.

    From the lesson Promises, await, Awaited and Cancellation

  10. 10. How would you keep a list of valid values and the type that describes them from drifting apart?

    Derive one from the other rather than writing both. I declare the list once with as const, which keeps each element's literal type and makes the array a readonly tuple, then take typeof LIST[number] as the union. Adding or removing a value changes the type automatically, and every exhaustive switch over that union fails to compile until the new case is handled — so the drift becomes a build error instead of a runtime surprise. The same idea with an object table: keyof typeof TABLE gives the keys and typeof TABLE[Key] gives the value shape. The trap is forgetting as const, because then the array is string[], the derived type is just string, and the derivation silently protects nothing — it still compiles, which is what makes it easy to miss in review.

    From the lesson Deriving Types: keyof, typeof and T[K]

  11. 11. What does homomorphic mean for a mapped type, and why should you care?

    A mapped type written directly over keyof T, with T a type parameter, is homomorphic, and the checker treats it specially: it preserves the existing optional and readonly modifiers on each property, it distributes over a union of object types, and it passes arrays and tuples through as arrays and tuples instead of turning them into objects with numeric keys. That is why Partial<string[]> is still an array-shaped type. If you write the keys any other way — over a literal union of names, or over some computed key set — you lose that preservation, so optional properties silently become required and arrays collapse. The practical rule is to map over keyof T whenever the intent is to transform a shape, and to reach for as only when the point is to rename keys or drop them by mapping to never.

    From the lesson Mapping Keys, Modifiers and Key Remapping

  12. 12. Why does Omit behave differently from Pick, and when does that matter?

    Pick<T, K> constrains K to keyof T, so a typo in a key name is a compile error, and it is written as a direct mapping. Omit is defined as Pick<T, Exclude<keyof T, K>> with K unconstrained — it accepts any key type at all, so Omit<User, "emial"> compiles and omits nothing, and that failure is completely silent. It is also not homomorphic, because the keys are computed rather than taken straight from keyof T, so it loses modifier and array preservation that Pick-style mappings keep. It matters most when deriving a public view of an entity: an Omit typo means an internal field you meant to strip is still in the response, and nothing in the build tells you. I prefer Pick where the field list is short, and where Omit is genuinely the right shape I define a constrained StrictOmit so the key must exist.

    From the lesson Partial to ReturnType: What the Standard Library Is Made Of

  13. 13. When do you use overloads instead of a union parameter or a generic?

    Only when the return type depends on the argument type. If a function takes string or number and always returns a string, a single signature with a union parameter is clearer and produces far better error messages. Overloads earn their place when string in means string[] out and number in means number[] out, because a union return would force every caller to narrow a result they already know the shape of. A generic with a conditional return can express the same relationship and composes better, but its diagnostics are worse, so for a small fixed set of shapes I prefer overloads. Two things to be careful about: resolution is first match top to bottom, so a general signature above a specific one silently makes the specific one unreachable; and nothing checks that the body honours each advertised signature — only that the implementation signature is broadly compatible — so I unit-test each overload.

    From the lesson Overload Signatures, Resolution Order and this

  14. 14. Why does { name: "Rex", breed: "collie" } fail inline but succeed through a variable?

    Because of excess property checking, which applies only to fresh object literals. Structurally the object is assignable — it has everything the target requires, and extra properties are irrelevant to assignability — so through a variable it is accepted. Written directly at the assignment or call, the literal is fresh, and TypeScript adds a deliberate extra check that unknown properties are an error. It exists because a target type with optional properties would otherwise accept any typo silently: misspell an optional option name and you get the default instead of a diagnostic. So it is a pragmatic typo-catcher rather than a rule of the type system, which is why it looks inconsistent. In practice I pass configuration objects inline precisely to keep that check, and where I must build them first I annotate the variable with the target type so the check happens at the declaration instead.

    From the lesson Structural Assignability, Freshness and Private Members

  15. 15. Why would DeepPartial break a type containing a Date, and how do you fix it?

    Because the recursion has no stopping case for built-ins. Date is an object type, so the mapping descends into it and makes every method optional, producing a type that a real Date is no longer assignable to — the error appears at the assignment, far from the helper. The same happens with Map, Set, RegExp, functions, and often arrays, where the mapping turns an array into an object with optional numeric keys. The fix is to order the branches so the special cases come first: match functions and the built-ins you care about and return them unchanged, handle arrays explicitly, then fall through to the object mapping, then to the primitive case. I would also bound the depth with a countdown tuple, because unbounded recursion on a genuinely deep shape ends in an instantiation-depth error that is far harder to read than the original problem.

    From the lesson Trees, JSON and Deep Helpers — With a Depth Limit

  16. 16. What does import type do, and when does its absence cause a real bug?

    It imports only the type side of a binding and guarantees the statement is erased from the output. The bug it prevents comes from import elision: TypeScript removes an ordinary import whose bindings are never used as values, so a module imported for a type — or imported for a side effect the author assumed would come along with it — can disappear from the emitted JavaScript. Frameworks that rely on module-level registration, decorators or polyfills are the usual victims, and the symptom is that everything works in development and the registration is missing from the production bundle. Being explicit fixes it in both directions: import type for types, and a bare side-effect import for modules whose effect matters. verbatimModuleSyntax goes further by emitting exactly what was written, which removes the guessing entirely and is what I would enable in a new project.

    From the lesson Imports, Exports, import type and Elision

  17. 17. An import type-checks in the editor and fails at build. What do you check?

    First, whether the resolution is a compile-time alias that nothing else knows about: tsconfig paths is understood by the compiler and by nothing at runtime, so if @app/user is declared there and not in the bundler or in Node's own resolution, the editor is happy and the build is not. Second, whether moduleResolution matches the consumer — NodeNext enforces Node's rules, including the ESM requirement that relative specifiers carry the emitted extension, while Bundler deliberately relaxes that; using the wrong one means the compiler is validating a different algorithm from the one that will run. Third, the package's exports map, because types and runtime entries resolve through separate conditions, and a package can resolve for code and not for types, or serve different files to import and require. If it is still unclear, tsc --traceResolution prints every path tried in order, which turns the question into reading a list rather than guessing.

    From the lesson From Specifier to File: Node16, Bundler and Path Mapping

  18. 18. Your bundler builds successfully but CI fails on types. What is going on, and how should the pipeline be arranged?

    The bundler never checked types. esbuild, SWC and Babel's TypeScript support all strip type syntax file by file and emit, with no type information and no program-level view, which is exactly why they are so fast. So a green bundler build says nothing about type correctness, and the pipeline needs tsc --noEmit as a separate step — in CI and in the editor — to be the source of truth. For a library, declarations need the checker too, so the build is usually a fast transpiler for code plus tsc --emitDeclarationOnly for the .d.ts files. I also turn on isolatedModules, because it makes the compiler reject the constructs single-file transpilation cannot handle correctly — re-exporting a type without export type, and const enums across modules — so the two tools cannot silently disagree about what the emit should be.

    From the lesson tsc, Babel, esbuild and the isolatedModules Contract

  19. 19. How would you arrange checks in CI for a TypeScript project, and why in that order?

    Cheapest and most deterministic first, so a trivial failure does not cost a full pipeline: formatting, then type-checking with tsc --noEmit, then lint including the type-aware rules, then tests, then an actual build. The ordering matters because a formatting failure that takes three seconds should not be discovered after a two-minute test run. The separation matters more: the bundler does not type-check, so a green build proves nothing about types, and the linter catches a class of bug the compiler cannot see at all — a floating promise is perfectly well typed. I pin the TypeScript version as a dev dependency and point the editor at the workspace version, because an editor and a CI on different compilers produce errors nobody can reproduce. And I give formatting entirely to Prettier so no lint rule argues with it, which is the usual cause of a CI that fails on files nobody touched.

    From the lesson Vite, ESLint, Prettier and the CI Gate

  20. 20. When would you use the compiler API rather than a lint rule or a script?

    When the question needs type information and no existing tool expresses it. Typical cases are generating an API-surface report and diffing it between releases to catch breaking changes, producing documentation or client code from types, and project-wide codemods that must rename or restructure code correctly rather than textually. The rule of thumb is that if a regular expression is about to parse code, the compiler API is the tool I actually wanted, because only a parser knows the difference between an identifier and the same letters inside a string or comment. For a rule that runs on every file in CI, I would write a typescript-eslint rule instead — it already owns the program, the caching and the reporting. And I pin the compiler version, because the API is not stable across releases: node shapes and factory helpers move, and a tool written against one version can simply stop working on the next.

    From the lesson Programs, Source Files, the Checker and Transformers

  21. 21. What makes a library's types pleasant to use, and what makes them painful?

    Pleasant means the caller never writes a type argument and never needs a cast: every type parameter is inferable from a value, constraints say exactly what the implementation reads, and the result types are named so hovering and errors are readable. Painful is usually one of four things. A type parameter nothing can infer, so every call carries an annotation — and the worst version, parse<T>(raw): T, which is a cast dressed as a generic and launders untrusted data. Unconstrained parameters, which force casts inside the implementation and produce errors that quote enormous anonymous structures. Positional parameters, especially booleans, which are unreadable at the call site and cannot be extended without a breaking change. And exporting internal helper types, because anything importable will be imported and then cannot be changed. I also test the public shapes with type-level assertions, since a diff that alters only the .d.ts still breaks consumers.

    From the lesson Inference-Friendly APIs, Error Messages and Extension Points

  22. 22. How do you type a reusable component so its callers cannot misuse it?

    Start from the fact that a component is a function from props to rendered output, so the props type is a parameter type and everything follows. I name the props type, make optional things genuinely optional with defaults, and model mutually exclusive states as a discriminated union rather than a bag of optional fields — so a loading component cannot also carry data, and the render callback exists only in the state that has something to render. For anything list-shaped I make it generic and let the item type flow from the items prop, so the render callback's parameter is inferred and a mismatch between columns and rows is a compile error. What I avoid is typing props as any, which switches off checking for the component and for every call site at once, and relying on prop spreading, since spreading skips the excess-property check that catches misspelled attribute names.

    From the lesson Components as Functions: Props, Children, Events and Generics

  23. 23. Where does TypeScript's guarantee end, and what do you do about it?

    It ends at every boundary where a value enters the program: request bodies and query parameters, environment variables, configuration files, webhook payloads, third-party responses, and anything read from storage that a different version of the code wrote. Inside the program the checker proves things; across the boundary it has no information at all, and an as cast there is a claim nobody verified — which is how a string "8080" becomes a port and an absent field becomes undefined three layers down. So I parse rather than cast: a parser takes unknown and returns either the typed value or a list of errors with field paths, which means the only way to hold the value is to have passed the check. I derive the static type from the schema so the two cannot drift, validate once at the boundary rather than at every layer, and treat any cast near a network or file read as a bug in review.

    From the lesson Parse, Do Not Cast: Schemas as the Source of Truth

  24. 24. How do you model the state of a screen that loads data?

    As a discriminated union rather than a set of independent flags. Three booleans and a nullable data field permit eight combinations, most of which the code cannot produce and all of which every component has to defend against — and the classic bug is showing "no results" while the request is still in flight, because empty and not-yet-loaded were collapsed into one condition. A union of idle, loading, ready and error permits exactly the states that exist, makes the data reachable only in the state that has it, and with a never check in the default branch a new state breaks every consumer that has not handled it. Alongside that I keep the server's shapes at one edge: a module that parses and maps API rows into view models, so a backend rename touches one function rather than every component, and validation happens in the same place.

    From the lesson State, Boundaries and the Shape of a UI

  25. 25. How do you change an API without breaking clients?

    By treating both deployment directions as real. The server and client are never updated at the same instant, so every change has to work for an old client against a new server and, during a rollback, for a new client against an old server. That makes adding optional response fields and widening accepted request types safe, while adding a required request field, removing a response field, or adding a value to a response enum is breaking — the last one surprises people, because an old client's exhaustive switch was correct when it was written. Structurally I keep one schema that both sides are generated from, and regenerate in CI so a drift is a failing build rather than a runtime cast that believes a renamed field. I give errors a stable machine-readable code so clients never branch on prose, use cursor pagination so inserts do not duplicate rows across pages, and require an idempotency key on unsafe operations so a timeout can be retried without double-charging.

    From the lesson Generated Clients, Versioning and Compatible Change

  26. 26. Does TypeScript make an application more secure?

    Marginally, and not in the way people hope. It eliminates a class of defects — wrong shapes, missing fields, null dereferences — that sometimes have security consequences, and it makes refactoring safe enough that security fixes are less risky to apply. What it does not do is anything about provenance: types are erased, so a malicious string and a safe one are the same type, and every injection vulnerability — SQL, command, path traversal, prototype pollution, XSS — type-checks perfectly. The defences are structural and unchanged by the language: parameterised queries, argument arrays instead of shell strings, path resolution checked against a root, rejecting dangerous keys on merge, escaping at render, and looking authorisation up server-side rather than reading it from the payload. Where types genuinely help is branding: a SafeHtml or ValidatedEmail type records that a check was run, so the only way to bypass it is an explicit cast that a reviewer or a lint rule can find.

    From the lesson Types Are Not Security: Injection, Escapes and Trust Boundaries

  27. 27. Which classical patterns survive in TypeScript, and which stop earning their keep?

    The ones about dependencies and data flow survive. Dependency inversion is as valuable as ever, but it costs a parameter rather than a container and an implements clause, because structural typing means any matching object qualifies. Repository earns its place when there really is a second implementation — an in-memory one for tests counts. Adapter earns it at the edge of an API I do not control. State machines and Command earn it when transitions have rules or actions must be queued and replayed. What stops earning its keep is most of the creational and behavioural ceremony: Strategy is a function parameter, Observer is a set of callbacks, a Factory for a single product is a function, and an interface with one implementation and no test double is a file that adds a name and an indirection without adding an ability. My rule before adding a pattern is to name the change it makes easy; if I cannot, it is decoration.

    From the lesson Dependency Inversion, Functional Core, and Patterns Worth Their Weight

  28. 28. Where does TypeScript deliberately choose unsoundness, and how do you manage the risk?

    In four places. Array and container covariance, so a Dog[] can be used as an Animal[] and then have a cat pushed into it. Method parameter bivariance, which strictFunctionTypes deliberately keeps because Array, Promise and the DOM declare their callbacks as methods. Index access, where list[0] and record[key] are typed as present even when they are not, unless noUncheckedIndexedAccess is on. And the explicit escape hatches — any, as, non-null assertions and type predicates — which are humans asserting things the compiler cannot verify. Each exists because the sound alternative would reject a great deal of correct, ordinary JavaScript. I manage it by treating the type system as excellent bug-finding rather than proof: turn on noUncheckedIndexedAccess, keep casts and predicates rare, test predicates that guard security decisions, and put real runtime validation exactly where data enters — because that is where the guarantee was never there in the first place.

    From the lesson Assignability, Variance, Freshness and the Unsound Edges

  29. 29. How would you design a typed event emitter, and what is the hard part?

    The core is one type mapping event names to payloads, and an API generic over its keys: on<K extends keyof Events>(event: K, handler: (payload: Events[K]) => void) and the matching emit. That makes a misspelled name an unknown-key error and a wrong payload a type error, with the handler's parameter inferred from the name — no type arguments at the call site, which is the test I apply to any framework surface. The hard part is extension: consumers want to add their own events, and the tempting answer is any, which unchecks everything for everyone. The honest options are a generic parameter threading the consumer's own event map through the emitter's type, or declaration merging on a registry interface so an application can augment the map. Both keep the surface open and the types intact. The related hard part is middleware chains, where composing steps means folding over a tuple of transformations; most frameworks cap it with overloads for a few lengths because the general version's error messages are unreadable.

    From the lesson Typed Routers, Emitters, Middleware and Plugin Systems

  30. 30. Walk me through diagnosing a production incident where the types were green.

    I start from the symptom and work back to the place where the compiler was told something it could not verify, because in a typed codebase that is almost always where these begin. Four shapes recur. A confident cast over external data, where the symptom is undefined or NaN appearing several layers from the boundary — the cause is a payload cast rather than parsed, and the control is parsing at the edge plus a rule against casts in modules that do I/O. A stale generated client, where a renamed field reads as undefined with full type confidence — the control is regenerating in CI and failing on a diff. A module-format mismatch, where a dual-published package is loaded twice so instanceof fails on an object that is obviously the right shape — the control is publishing one format. And unreadable stack traces, where source maps were generated but never uploaded. In each case I close the incident with the control rather than the fix, because correcting the cast leaves the mechanism intact and the next rename reproduces it exactly.

    From the lesson Reviewing What the Compiler Missed, and Debugging What It Allowed

Learn it, then drill it

The TypeScript course teaches every one of these in context, with runnable examples. For drilling, the practice bank has 2,595 multiple-choice TypeScript questions with explanations.

TypeScript topic pages

Other subjects