These 28 TypeScript interview questions focus on the type system and its tooling: narrowing, generics, conditional and mapped types, tsconfig strictness, declaration files and builds at scale. Runtime behavior such as closures and the event loop is covered on our JavaScript page. When we evaluate senior TypeScript engineers at Ryz, recruiters source people who have typed real codebases, candidates complete structured NTRVSTA AI interviews, and recruiters review the results before and after. AI scores inform the process. People decide.
The gap between someone who writes TypeScript and someone who designs with it is large. Mid-level candidates should handle Fundamentals and most of Intermediate without hesitation. For senior and library-focused roles, spend time on Advanced types and Senior and architecture, and ask them to type something on screen rather than describe it.
Reward judgment as much as cleverness. A candidate who writes a five-level conditional type when a union would do has shown skill and poor taste in the same answer. Ask "who has to read this type in six months?" and listen to the reply.
any, unknown and never?any turns off checking in both directions and spreads silently. unknown accepts any value but forces you to narrow before use, which makes it the right type for parsed JSON, caught errors and external input. never has no values: it is the return type of functions that always throw and the result of narrowing away every case.
What a strong answer shows: They default to unknown at boundaries and know useUnknownInCatchVariables under strict types caught errors as unknown.
type versus interface?Both describe object shapes. Interfaces support declaration merging and extends, and give clearer error messages for object hierarchies. Type aliases are needed for unions, tuples, mapped and conditional types. Many teams use interfaces for public object contracts and types for everything else.
What a strong answer shows: They mention declaration merging as both the reason to use interfaces for augmentation and a risk of accidental merging.
Excess property checking only applies to fresh object literals assigned directly to a typed target, because an unknown key there is likely a typo. A variable has already been widened, and structural typing allows extra properties.
What a strong answer shows: They can explain structural compatibility clearly and know the check is a lint-like safeguard, not part of assignability itself.
Use a discriminated union and assign the remaining value to never in the default branch. Adding a new member then fails to compile everywhere it is not handled.
type Shape =
| { kind: "circle"; r: number }
| { kind: "rect"; w: number; h: number };
function area(s: Shape): number {
switch (s.kind) {
case "circle": return Math.PI * s.r ** 2;
case "rect": return s.w * s.h;
default: {
const unhandled: never = s;
throw new Error(`Unhandled shape: ${JSON.stringify(unhandled)}`);
}
}
}
What a strong answer shows: They model state as discriminated unions instead of objects full of optional fields.
strict, and why?noUncheckedIndexedAccess adds undefined to index and record lookups, catching a common class of crashes. exactOptionalPropertyTypes distinguishes a missing property from one set to undefined. noImplicitOverride and noFallthroughCasesInSwitch are cheap wins.
What a strong answer shows: They know what each flag catches and how to roll one out on an existing codebase without a giant pull request.
enum?Enums emit runtime code and numeric enums accept any number. A union of string literals, or an as const object with a derived union, is usually simpler and works with tools that only strip types, such as Node's built-in type stripping. TypeScript 5.8 added --erasableSyntaxOnly to flag enums and other non-erasable syntax.
What a strong answer shows: They know the reasons, not just a rule, and handle existing enums pragmatically.
satisfies do that a type annotation does not?An annotation replaces the inferred type with the declared one. satisfies checks the value against a type but keeps the narrower inferred type.
type Route = { path: `/${string}`; auth: boolean };
const routes = {
home: { path: "/", auth: false },
billing: { path: "/billing", auth: true },
} satisfies Record<string, Route>;
routes.billing.path; // OK: TypeScript still knows the keys
// routes.biling // error, unlike with a Record<string, Route> annotation
What a strong answer shows: They contrast it with as, which silences errors rather than checking.
pluck function that only accepts real keys.function pluck<T, K extends keyof T>(items: readonly T[], key: K): T[K][] {
return items.map((item) => item[key]);
}
The constraint K extends keyof T rejects unknown keys, and the indexed access type T[K] gives the precise return type.
What a strong answer shows: They let inference do the work at call sites and keep generic parameters to the minimum needed.
A function returning value is User tells the compiler to narrow when it returns true. Assertion functions using asserts value is User narrow after the call or throw. The compiler trusts the implementation completely, so a guard that checks one field narrows to a type the value may not satisfy.
What a strong answer shows: They prefer schema validation for external data, and know TypeScript 5.5 can infer simple type predicates from function bodies such as filter callbacks.
Types disappear at runtime, so a cast on response.json() is a promise nobody checks. Parse with a schema library such as Zod, Valibot or ArkType at the boundary and infer the static type from the schema, or generate both from an OpenAPI spec.
What a strong answer shows: One source of truth for shape and type, with a decision on whether failures throw, log or degrade.
Overloads fit when the return type depends on the argument in ways a single generic signature cannot express cleanly, such as createElement("canvas") returning HTMLCanvasElement. If one signature with a union or generic works, prefer it: overloads are matched in order and their implementation signature is not visible to callers.
What a strong answer shows: They know overload resolution order matters and that conditional return types often need casts in the body.
Readonly<T> go?It is shallow: nested objects stay mutable. Use readonly arrays and tuples, as const for literal data, or a recursive DeepReadonly type when needed. None of these freeze anything at runtime.
What a strong answer shows: They accept readonly T[] in function parameters so callers can pass both mutable and readonly arrays.
Omit behave strangely on a union type?Omit<A | B, "id"> works on keyof (A | B), which is only the keys common to both, so the result loses the discriminant's variant-specific fields. A distributive version fixes it: type DistributiveOmit<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never.
What a strong answer shows: They understand why keyof on a union is the intersection of keys.
module and moduleResolution settings do you use, and why do they matter?For code run by Node, nodenext, which follows Node's ESM and CommonJS rules including file extensions and exports maps. For code bundled by Vite or similar, bundler. verbatimModuleSyntax makes type-only imports explicit with import type, so single-file transpilers emit correct code.
What a strong answer shows: They have debugged "works in the editor, fails at runtime" import issues and know the compiler does not rewrite import paths by default.
type ToArray<T> = T extends unknown ? T[] : never;
type A = ToArray<string | number>; // string[] | number[]
type ToArrayAll<T> = [T] extends [unknown] ? T[] : never;
type B = ToArrayAll<string | number>; // (string | number)[]
A conditional type on a naked type parameter distributes over each union member. Wrapping both sides in a tuple prevents it.
What a strong answer shows: They know never is the empty union, so a distributive type applied to never returns never.
infer do? Give a practical use.infer declares a type variable inside the extends clause of a conditional type and captures part of the matched type. Built-ins such as ReturnType, Parameters and Awaited use it. A practical case: type ElementOf<T> = T extends readonly (infer U)[] ? U : never to get the item type of a config array.
What a strong answer shows: They reach for built-in utilities first and write custom infer types only when needed.
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = Getters<{ name: string; age: number }>;
// { getName: () => string; getAge: () => number }
The as clause remaps keys, and string & K filters out number and symbol keys that Capitalize cannot accept.
What a strong answer shows: They know as never in a key remap drops properties, which is how filtering mapped types work.
type Params<P extends string> =
P extends `${string}:${infer Name}/${infer Rest}`
? Name | Params<`/${Rest}`>
: P extends `${string}:${infer Name}` ? Name : never;
type T = Params<"/users/:id/posts/:postId">; // "id" | "postId"
What a strong answer shows: They understand template literal inference and recursion, and know when type-level parsing becomes a compile-time cost not worth paying.
UserId being passed where an OrderId is expected when both are strings?Use branded types: intersect the primitive with a phantom property, and create values only through a validating constructor.
declare const brand: unique symbol;
type Brand<T, B extends string> = T & { readonly [brand]: B };
type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
const toUserId = (raw: string): UserId => raw as UserId;
What a strong answer shows: They keep the cast in one place and use brands for IDs, currencies and validated strings, not everywhere.
Dog[] assignable to Animal[] even though that is unsound?TypeScript treats arrays and method parameters bivariantly for practicality, so you can push a Cat into what is really a Dog[]. strictFunctionTypes checks function-typed properties contravariantly, but not method-shorthand signatures. Variance annotations (in, out) on type parameters exist mainly to document and speed up checks.
What a strong answer shows: They know where the type system deliberately trades soundness for usability and use readonly arrays to avoid the hole.
Run tsc --extendedDiagnostics to see time and instantiation counts, then --generateTrace and analyze it to find the expensive files and types. Usual causes are large unions in deeply recursive conditional types, inferred return types that are huge, and missing project references. Add explicit return type annotations on exported functions and simplify hot types.
What a strong answer shows: They measure before refactoring and mention incremental builds and project references for the structural fix.
Enable allowJs and checkJs gradually, convert leaf modules first, type shared boundaries early, and turn strict flags on per directory with separate configs or a baseline of existing errors. Ban new any with lint rules, track the count, and avoid a freeze on feature work.
What a strong answer shows: A plan that ships every week, with metrics, and an honest view of where any is acceptable temporarily.
// types/legacy-charts.d.ts
declare module "legacy-charts" {
export function render(el: HTMLElement, data: number[]): void;
}
// types/express.d.ts
import "express-serve-static-core";
declare module "express-serve-static-core" {
interface Request { user?: { id: string; roles: string[] } }
}
An ambient module declaration covers the untyped package. Module augmentation merges into an existing interface, which requires the file to be a module, hence the import.
What a strong answer shows: They type only what they use, and consider contributing types to DefinitelyTyped.
moduleResolution setting?Ship compiled JavaScript with declaration files, an exports map with a types condition for each entry point (listed first), and separate declarations for ESM and CommonJS builds if you ship both. Verify with a tool such as Are the Types Wrong, and test consumption under nodenext and bundler.
What a strong answer shows: They have hit dual-package problems before and treat the published types as a public API with semver.
Use project references with composite and build with tsc -b so packages are checked incrementally against emitted declarations. isolatedDeclarations requires explicit types on exports so declarations can be generated in parallel by other tools. Share one base tsconfig.
What a strong answer shows: They understand why path aliases that bypass package boundaries hurt build performance and ownership.
Options include generating clients from an OpenAPI spec, end-to-end typed RPC such as tRPC within one monorepo, or shared schema packages. Never share database row types directly with the client, since that couples the UI to storage and can leak fields.
What a strong answer shows: They choose based on team boundaries and versioning, not only developer convenience.
Usually tsc --noEmit for type checking in CI and the editor, with esbuild, SWC or the bundler doing the emit for speed. That requires isolatedModules. Recent Node versions can run erasable TypeScript directly by stripping types. The team is also building a native Go port of the compiler, which a senior candidate may have tried.
What a strong answer shows: They separate type checking from transpilation and know which syntax each tool cannot handle.
Keep public types simple and named, so hover text and errors are readable. Prefer discriminated unions and overloads with clear names over clever inference, test types with expectTypeOf or @ts-expect-error cases, and treat type changes as breaking changes.
What a strong answer shows: They think about the consumer's error messages, which is the real user interface of a type.
as or any to silence errors and calls the code type-safe.await res.json() to a type and assumes the data matches.strict enabled.// @ts-ignore instead of @ts-expect-error with a comment.Give a 3-hour take-home: a small, loosely typed JavaScript module (an event bus plus a fetch-based API client with three endpoints) to convert to strict TypeScript. Requirements: a typed event bus where emit("order:paid", payload) checks the payload against the event name, runtime validation of API responses with a schema library, branded IDs, and type tests that fail if the types regress. Include a tsconfig with strict and noUncheckedIndexedAccess.
any or unchecked casts beyond clearly justified ones.Need engineers who already work this way? Hire senior TypeScript developers through Ryz: top 1% of the candidates we interview, working on your team and repos, within ±1h of US time zones. Our vetting process explains how we assess them, and our TypeScript developer job description is ready to adapt if you are hiring on your own.
For application roles, one or two at most. Most product code needs solid narrowing, generics and good tsconfig habits. Library and platform roles justify deeper conditional and mapped type questions because those engineers ship types other teams depend on.
Share an editor with the TypeScript language service running, such as the TypeScript Playground or a small repo, and have the candidate fix real type errors or type an existing function. Watching how they read compiler errors tells you more than any verbal answer.
Basic TypeScript is quick to learn for a strong JavaScript engineer. Designing types for a large codebase, migrating it, and keeping builds fast take longer, so for those responsibilities hire someone who has done it before.
Questions we didn't answer? Email info@ryzlabs.com.