
These are the TypeScript interview questions I’d study before a frontend loop in 2026: 120 of them, grouped by topic and ordered from basic to advanced. Every question has a direct answer and a short code sample, and the type challenges at the end come with full solutions you can paste into a TypeScript playground.
TypeScript interviews changed in the last year. TypeScript 6.0 turned strict on by default, and TypeScript 7.0 shipped a native compiler that is roughly 10x faster. Interviewers still mostly test the same thing, though: can you model data so the compiler catches bugs for you, and do you know where types stop protecting you at runtime. The answers follow the official TypeScript Handbook.
What to Expect
- 8 sections, from type basics and narrowing to generics, utility types, conditional and mapped types, tsconfig, and React.
- Direct answers first, then a code sample, a comparison table, or the pitfall interviewers look for.
- Advanced TypeScript interview questions for experienced developers:
infer, variance, branded types, and declaration merging. - TypeScript coding interview questions: 12 type challenges (
MyPick,DeepReadonly,TupleToUnion, and more) with solutions.
Every FrontendLead coding question can also be solved in TypeScript: switch the editor language to TS. Pair this list with the JavaScript interview questions and React interview questions guides, since TypeScript rounds test both.
Jump to a section
- TypeScript Basics (Q1-22)
- Narrowing and Type Guards (Q23-34)
- Functions and Generics (Q35-50)
- Utility Types (Q51-64)
- Advanced TypeScript Interview Questions (Q65-84)
- Modules, Declaration Files, and tsconfig (Q85-96)
- React TypeScript Interview Questions (Q97-108)
- TypeScript Coding Interview Questions: Type Challenges (Q109-120)
- How to Use This List
Test yourself afterwards with the TypeScript quiz on Quiz Yourself.
TypeScript Basics

Find in-depth explanations and practice here: JavaScript Interview Questions and Answers ->
Basics questions check two things: that you know types are erased before the code runs, and that you pick the right tool among type, interface, unions, and literal types.
1. What is TypeScript, and what does the compiler actually do?
TypeScript is JavaScript plus a static type system. The compiler (tsc) does two separate jobs: it type-checks your code, and it emits JavaScript with the types removed. Types don’t exist at runtime, so TypeScript never validates an API response for you, and a type error doesn’t stop emit unless you set noEmitOnError.
const port: number = 3000;
// Emitted JavaScript: const port = 3000;
2. Why use TypeScript instead of plain JavaScript?
It catches a whole class of bugs before runtime: typos in property names, calling something that might be undefined, passing the wrong shape to a function. The bigger win on large teams is tooling: autocomplete, safe renames, and types that act as documentation that can’t drift from the code. The cost is a build or type-check step and some learning curve for advanced types.
3. What is the difference between type and interface?
Both can describe object shapes, and for most objects they’re interchangeable. The differences:
| Feature | interface |
type |
|---|---|---|
| Object shapes | Yes | Yes |
| Unions, primitives, tuples | No | Yes |
| Mapped and conditional types | No | Yes |
| Declaration merging (re-open) | Yes | No (duplicate identifier error) |
| Extending | extends |
& intersection |
The handbook’s rule of thumb: use interface until you need a feature only type has. Many teams just pick one per codebase for consistency, which is also a fine answer if you can explain the trade-off.
interface User { id: string; name: string }
interface Admin extends User { permissions: string[] }
type Status = "idle" | "loading" | "error"; // only possible with type
type AdminT = User & { permissions: string[] };
4. What is the difference between any, unknown, and never?
any turns type checking off for a value. unknown accepts any value too, but you must narrow it before you use it, so it’s the safe type for data you don’t control. never is the type with no values: the return type of a function that always throws, and what’s left after every case of a union is handled.
const a: any = JSON.parse("{}");
a.foo.bar(); // compiles, crashes at runtime
const u: unknown = JSON.parse("{}");
// @ts-expect-error 'u' is of type 'unknown'
u.foo;
if (typeof u === "object" && u !== null && "foo" in u) {
console.log(u.foo); // OK after narrowing
}
function fail(msg: string): never {
throw new Error(msg);
}
What the interviewer is checking: that you reach for unknown, not any, at boundaries like JSON.parse, fetch, and catch clauses.
5. What is the difference between void and undefined as return types?
void means “the caller shouldn’t use the return value”. undefined means the function returns the value undefined. The practical difference is in callback types: a function type returning void accepts functions that return anything, which is why arr.forEach((x) => list.push(x)) compiles even though push returns a number.
type Callback = () => void;
const cb: Callback = () => 42; // OK: return value is ignored
const list: number[] = [];
[1, 2].forEach((x) => list.push(x)); // OK
6. What are the primitive types in TypeScript?
The same seven as JavaScript: string, number, bigint, boolean, symbol, null, and undefined. Always use the lowercase names. String, Number, and Boolean refer to the wrapper objects and accept things you almost never want. object (lowercase) means “any non-primitive”, which is different from {} (any non-nullish value, primitives included).
7. What is type inference?
TypeScript works out a type from the value when you don’t annotate it. Variables get the type of their initializer, functions get a return type from their return statements, and callbacks get parameter types from where they’re passed (contextual typing). Good TypeScript annotates function parameters and public APIs and lets inference handle the rest.
let count = 0; // number
const mode = "dark"; // "dark" (const keeps the literal)
const nums = [1, 2, 3]; // number[]
nums.map((n) => n * 2); // n is number via contextual typing
8. What are union types?
A union A | B is a value that can be either type. You can only use members that exist on every type in the union until you narrow it. Unions are the most common way to model “one of these states” in TypeScript.
function format(id: string | number) {
// @ts-expect-error toUpperCase does not exist on type number
id.toUpperCase();
return typeof id === "string" ? id.toUpperCase() : id.toFixed(0);
}
9. What are intersection types?
An intersection A & B has all members of both types. It’s how you combine object types with type aliases. If the two types conflict on a property (for example string and number), that property becomes never, and intersecting two unrelated primitives gives never outright.
type WithId = { id: string };
type WithTimestamps = { createdAt: Date };
type Entity = WithId & WithTimestamps; // { id; createdAt }
type Impossible = string & number; // never
10. What are literal types?
A literal type is a single exact value used as a type: "GET", 404, or true. Unions of literals replace most enums and make invalid states unrepresentable. Note that let widens a literal to its base type while const keeps it.
type Method = "GET" | "POST";
function request(url: string, method: Method) {}
let m = "GET"; // string
const c = "GET"; // "GET"
// @ts-expect-error string is not assignable to Method
request("/api", m);
request("/api", c); // OK
11. What are tuples?
A tuple is an array with a fixed length and a known type at each position. They support optional elements, rest elements, and labels, and they’re how hooks like useState return two differently typed values.
type Point = [x: number, y: number];
type Args = [string, ...number[]];
type MaybePair = [string, number?];
const p: Point = [1, 2];
const [x, y] = p; // x: number, y: number
12. What is the difference between an enum and a union of string literals?
An enum is one of the few TypeScript features that emits runtime code: an object mapping names to values. A union of literals is erased completely. Most modern codebases prefer unions (or an as const object when they need runtime values) because they’re lighter, work with plain strings from APIs, and run under Node’s type stripping and the erasableSyntaxOnly flag, which reject enums.
| Aspect | enum |
String literal union |
|---|---|---|
| Runtime code | Yes, an object | None |
| Accepts plain strings | No, must use Color.Red |
Yes |
| Iterate values | Object.values(Color) |
Needs an as const array |
| Works with type stripping | No | Yes |
const COLORS = ["red", "green", "blue"] as const;
type Color = (typeof COLORS)[number]; // "red" | "green" | "blue"
enum Direction { Up = "UP", Down = "DOWN" }
const d: Direction = Direction.Up;
13. What is the difference between numeric and string enums?
Numeric enums auto-increment from 0 and get a reverse mapping, so Status[0] returns "Active". String enums have no reverse mapping and every member needs an explicit value. Numeric enums were historically loose about which numbers they accept; since TypeScript 5.0, assigning an out-of-range literal like 99 to a numeric enum type is an error.
enum Status { Active, Inactive } // 0, 1
console.log(Status[0]); // "Active"
console.log(Status.Inactive); // 1
14. What is a const enum, and why do people avoid it?
A const enum is inlined at compile time: Direction.Up becomes "UP" in the output and no object is emitted. It breaks with tools that compile one file at a time (Babel, esbuild, SWC, isolatedModules) because they can’t see the enum’s values in another file. A union of literals gives the same zero-runtime result without that problem.
15. What is a type assertion (as), and when is it safe?
A type assertion tells the compiler “trust me, this value has this type”. It changes nothing at runtime and doesn’t convert anything. It’s safe only when you know more than the compiler, for example a DOM element you just created. TypeScript blocks assertions between unrelated types unless you go through unknown, which is a code smell in reviews.
const input = document.getElementById("email") as HTMLInputElement;
// @ts-expect-error neither type sufficiently overlaps
const n = "hello" as number;
const forced = "hello" as unknown as number; // compiles, but it's a lie
16. What does the non-null assertion operator (!) do?
The postfix ! removes null and undefined from a type without any runtime check. If you’re wrong, you get the same runtime crash TypeScript was supposed to prevent. Prefer a real check, optional chaining, or a function that throws a clear error.
const el = document.querySelector("#app")!; // Element, unchecked
const root = document.querySelector("#app");
if (!root) throw new Error("#app not found");
root.append("ok"); // narrowed, and the failure is explicit
17. What are optional and readonly properties?
? makes a property optional, so its type includes undefined and it may be missing. readonly blocks reassignment through that type at compile time only. It’s shallow: a readonly array property can’t be reassigned, but its contents can be pushed to unless the type is readonly string[].
interface Config {
readonly apiUrl: string;
timeout?: number; // number | undefined
readonly tags: readonly string[];
}
const cfg: Config = { apiUrl: "/api", tags: ["a"] };
// @ts-expect-error apiUrl is read-only
cfg.apiUrl = "/v2";
// @ts-expect-error push does not exist on readonly string[]
cfg.tags.push("b");
18. What is an index signature?
An index signature types objects whose keys aren’t known in advance: { [key: string]: number }. Every named property must then match the index type. By default, reading obj[anyKey] returns number, not number | undefined; turn on noUncheckedIndexedAccess to make that honest.
interface Scores {
[player: string]: number;
}
const scores: Scores = { ana: 10 };
const bob = scores["bob"]; // number, but actually undefined at runtime
19. What is structural typing?
TypeScript compares types by shape, not by name. Any value with the required properties is assignable, whatever it was declared as. That’s the opposite of Java or C#, which are nominal. It’s why you need branded types (Q80) when two values share a shape but must not be mixed.
interface Point { x: number; y: number }
class Vec { constructor(public x: number, public y: number) {} }
const p: Point = new Vec(1, 2); // OK: same shape
20. What is excess property checking?
When you assign an object literal directly to a typed target, TypeScript errors on properties the target doesn’t declare. It’s a typo check that only applies to fresh object literals. Assign the same object through a variable first and the extra property is allowed, because structural typing only requires the declared properties.
interface Options { size: number; color?: string }
// @ts-expect-error 'colour' does not exist in type Options
const o1: Options = { size: 1, colour: "red" };
const raw = { size: 1, colour: "red" };
const o2: Options = raw; // OK: not a fresh literal
One exception: if every property of the target is optional (a “weak type”), TypeScript also rejects any value that shares no properties with it, even through a variable.
21. What is the difference between null and undefined in TypeScript?
Both are separate types. With strictNullChecks on (part of strict), neither is assignable to string or other types unless you include it in a union. Optional properties and parameters add undefined, not null. Many teams use undefined for “missing” and reserve null for values that come from APIs or the DOM.
Deep dive: null vs undefined vs undeclared
22. How do you type arrays and objects you build up step by step?
Annotate the empty value. An unannotated [] is an “evolving” array whose type depends on what you push later, inside an object literal like { items: [] } it’s typed never[], and an empty object literal is typed {}, so you can’t add keys to it. Annotating up front gives you checking on every push or assignment.
const ids: string[] = [];
ids.push("a");
const byId: Record<string, { name: string }> = {};
byId["a"] = { name: "Ana" };
Narrowing and Type Guards

Find in-depth explanations and practice here: typeof, instanceof, and Object.prototype.toString ->
Narrowing is where TypeScript meets runtime JavaScript. Interviewers use these questions to see whether you model state with discriminated unions and whether you know which checks the compiler actually understands.
23. What is type narrowing?
Narrowing is the compiler refining a broad type to a more specific one inside a branch, based on a runtime check it understands. After if (typeof x === "string"), x is string in that block and the remaining union members after it. TypeScript tracks this through control flow: if, switch, early return, &&, and assignments.
function len(x: string | string[] | null) {
if (x === null) return 0;
if (typeof x === "string") return x.length; // string
return x.length; // string[]
}
24. Which checks narrow a type?
typeoffor primitives ("string","number","function", …).instanceoffor classes and built-ins likeDateorError.- The
inoperator for object shapes. - Equality (
===,!==,== null) and truthiness. - Discriminant property checks in a
switch. - User-defined type guards and assertion functions.
The classic trap: typeof null === "object", so typeof x === "object" doesn’t remove null. TypeScript knows this and keeps null in the narrowed type.
function f(x: object | null) {
if (typeof x === "object") {
// @ts-expect-error 'x' is possibly 'null'
Object.keys(x);
}
}
25. What is a discriminated union?
A union of object types that share a literal property, the discriminant (usually kind, type, or status). Checking the discriminant narrows to exactly one member. It’s the standard way to model state machines, API results, and reducer actions, because impossible combinations (like loading with data) can’t be built.
type Result =
| { status: "loading" }
| { status: "success"; data: string[] }
| { status: "error"; error: Error };
function render(r: Result) {
switch (r.status) {
case "loading": return "Loading...";
case "success": return r.data.join(", ");
case "error": return r.error.message;
}
}
26. How do you make a switch exhaustive?
Assign the value to a never variable in the default branch. If someone adds a new member to the union and forgets a case, the leftover type isn’t never anymore and the build fails at that line.
type Shape = { kind: "circle"; r: number } | { kind: "square"; size: number };
function assertNever(x: never): never {
throw new Error("Unhandled: " + JSON.stringify(x));
}
function area(s: Shape) {
switch (s.kind) {
case "circle": return Math.PI * s.r ** 2;
case "square": return s.size ** 2;
default: return assertNever(s);
}
}
What the interviewer is checking: that you use the compiler to catch the “forgot to handle the new case” bug, instead of relying on tests.
27. What is a user-defined type guard (type predicate)?
A function whose return type is value is Type. When it returns true, TypeScript narrows the argument to that type in the caller. The compiler trusts your implementation, so a wrong check gives you wrong types with no error.
interface Fish { swim(): void }
interface Bird { fly(): void }
function isFish(pet: Fish | Bird): pet is Fish {
return "swim" in pet;
}
function move(pet: Fish | Bird) {
if (isFish(pet)) pet.swim();
else pet.fly();
}
28. How do you filter null out of an array with correct types?
Before TypeScript 5.5 you needed an explicit type predicate, because filter(Boolean) and filter((x) => x !== null) returned the original element type. TypeScript 5.5 added inferred type predicates, so filter((x) => x !== null) now narrows on its own. filter(Boolean) still doesn’t, since Boolean isn’t a predicate.
const raw: (string | null)[] = ["a", null, "b"];
const clean = raw.filter((x): x is string => x !== null); // string[] in every version
29. What is an assertion function?
A function with an asserts return type. Instead of returning a boolean, it throws when the check fails, and after the call TypeScript treats the condition as true for the rest of the scope. It’s a clean way to validate inputs at the top of a function.
function assertIsString(v: unknown): asserts v is string {
if (typeof v !== "string") throw new TypeError("Expected string");
}
function shout(v: unknown) {
assertIsString(v);
return v.toUpperCase(); // v: string
}
30. How does the in operator narrow types?
"key" in obj narrows a union to the members that declare key (as required or optional). Since TypeScript 4.9, it also narrows types that don’t declare the key at all, by adding Record<"key", unknown>, which is what makes validating unknown objects practical.
function hasId(x: unknown) {
if (typeof x === "object" && x !== null && "id" in x) {
return x.id; // x: object & Record<"id", unknown>
}
}
31. Why do types not narrow inside callbacks?
Narrowing of a let variable or a mutable property doesn’t carry into a callback, because the callback might run later, after the value changed. Copy the narrowed value into a const first. A const variable stays narrowed in closures, and TypeScript 5.4 extended that to let variables and parameters that aren’t reassigned after the closure is created.
function run(user: { name?: string }) {
if (user.name) {
const name = user.name; // string, safe to capture
setTimeout(() => console.log(name.toUpperCase()), 0);
}
}
32. What type does a catch clause variable have?
unknown when useUnknownInCatchVariables is on, which strict enables. Anything can be thrown in JavaScript, not only Error objects, so you narrow before reading .message.
try {
JSON.parse("{bad");
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
console.log(msg);
}
33. What is control flow analysis for aliased conditions?
Since TypeScript 4.4, a check stored in a const still narrows when you use that const later. It makes complex conditions readable without losing type information. It only works when both the condition and the narrowed value are const (or readonly properties, or parameters that aren’t reassigned).
function f(x: string | number) {
const isStr = typeof x === "string";
if (isStr) return x.toUpperCase(); // x: string
return x.toFixed(2); // x: number
}
34. How do you validate unknown API data so the types are real?
Treat the response as unknown and parse it with a runtime validator. A schema library like Zod gives you the type and the check from one definition, so they can’t drift. Casting await res.json() as User only silences the compiler; if the API changes, the bug surfaces far away from the fetch.
interface User { id: string; name: string }
function isUser(v: unknown): v is User {
return typeof v === "object" && v !== null &&
"id" in v && typeof v.id === "string" &&
"name" in v && typeof v.name === "string";
}
async function getUser(id: string): Promise<User> {
const data: unknown = await (await fetch(`/api/users/${id}`)).json();
if (!isUser(data)) throw new Error("Invalid user payload");
return data;
}
What the interviewer is checking: that you know types end at the network boundary.
Functions and Generics

Find in-depth explanations and practice here: Curry Function (solve it in TypeScript) ->
Generics questions separate people who use TypeScript from people who design types for others. Expect to write a small generic function on the spot and explain why each type parameter is there.
35. How do you type a function?
Annotate the parameters and, for public functions, the return type. Function types use arrow syntax: (a: number) => string. Optional parameters use ? and must come after required ones; default values make a parameter optional and infer its type.
function greet(name: string, greeting = "Hi", punct?: string): string {
return `${greeting}, ${name}${punct ?? "!"}`;
}
type Formatter = (value: number, locale?: string) => string;
const usd: Formatter = (v) => `$${v.toFixed(2)}`;
36. What are generics, and why do you need them?
Generics are type parameters: they let a function, class, or type work with many types while keeping the relationship between input and output. Without them you’d choose between duplicating code per type or using any and losing the link. identity<T>(x: T): T says “whatever goes in comes out”, which (x: any) => any can’t express.
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
const n = first([1, 2, 3]); // number | undefined
const s = first(["a", "b"]); // string | undefined
37. What are generic constraints?
T extends X limits which types can be passed for T, which lets the function body use members of X. Without the constraint, TypeScript only lets you do things that are valid for every possible type.
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}
longest("abc", "de"); // string
longest([1, 2], [1, 2, 3]); // number[]
// @ts-expect-error number has no length
longest(1, 2);
38. How do you write a type-safe getProperty(obj, key)?
Constrain the key to keyof T and return the indexed access type T[K]. This is the most common generics question in TypeScript interviews, because it combines constraints, keyof, and indexed access in one line.
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: "Ana" };
const name = getProperty(user, "name"); // string
// @ts-expect-error "email" is not a key of user
getProperty(user, "email");
39. What are default type parameters?
A type parameter can have a default, like a function parameter: <T = string>. It’s used when the caller doesn’t pass the type and inference has nothing to go on. Parameters with defaults must come after required ones.
interface ApiResponse<T = unknown, E = Error> {
data?: T;
error?: E;
}
const r1: ApiResponse = {}; // data: unknown
const r2: ApiResponse<string[]> = { data: [] };
40. What are function overloads?
Overloads are multiple call signatures for one function, followed by a single implementation signature that callers can’t see. Use them when the return type depends on the argument type in a way a union can’t express. If a union parameter or a generic works, prefer that: overloads are more code and the implementation isn’t checked against each signature precisely.
function parse(input: string): number;
function parse(input: string[]): number[];
function parse(input: string | string[]): number | number[] {
return Array.isArray(input) ? input.map(Number) : Number(input);
}
const a = parse("1"); // number
const b = parse(["1", "2"]); // number[]
41. How do you type this in a function?
Declare a fake first parameter named this. It’s erased in the output and only tells the compiler what this must be at call sites. With noImplicitThis (part of strict), using this in a plain function without it is an error.
interface Button { label: string }
function onClick(this: Button, e: Event) {
console.log(this.label, e.type);
}
const btn: Button = { label: "Save" };
onClick.call(btn, new Event("click"));
Deep dive: Explain “this”
42. How do you type rest parameters and a variadic function?
Type the rest parameter as an array or tuple. Generic tuples (T extends unknown[]) let you capture any argument list and forward it with the exact types, which is how you type wrappers like debounce or memoize.
function sum(...nums: number[]) {
return nums.reduce((a, b) => a + b, 0);
}
function logged<A extends unknown[], R>(fn: (...args: A) => R) {
return (...args: A): R => {
console.log("calling with", args);
return fn(...args);
};
}
const add = logged((a: number, b: number) => a + b);
add(1, 2); // (a: number, b: number) => number
43. How would you type a debounce function?
Capture the parameter list as a generic tuple and return a function with the same parameters that returns void (the caller can’t get the result synchronously). ReturnType<typeof setTimeout> keeps the timer type correct in both browser and Node typings.
function debounce<A extends unknown[]>(fn: (...args: A) => void, ms: number) {
let timer: ReturnType<typeof setTimeout> | undefined;
return (...args: A): void => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), ms);
};
}
const save = debounce((id: string, draft: boolean) => {}, 300);
save("doc-1", true);
// @ts-expect-error argument types are preserved
save(1);
Practice: Debounce I
44. How do you type a memoize function?
The same generic tuple pattern: the returned function has the exact signature of the input. The cache key is where you make a design decision; JSON.stringify(args) is the simple interview answer, with the caveat that it fails for functions, undefined vs missing, and object key order.
function memoize<A extends unknown[], R>(fn: (...args: A) => R): (...args: A) => R {
const cache = new Map<string, R>();
return (...args) => {
const key = JSON.stringify(args);
if (!cache.has(key)) cache.set(key, fn(...args));
return cache.get(key)!;
};
}
Practice: Memoize I
45. How do generic classes work?
A class can take type parameters that apply to its instance members. Static members can’t use the class’s type parameters, since there’s only one static side for all instantiations.
class Queue<T> {
private items: T[] = [];
enqueue(item: T) { this.items.push(item); }
dequeue(): T | undefined { return this.items.shift(); }
}
const q = new Queue<number>();
q.enqueue(1);
// @ts-expect-error string is not assignable to number
q.enqueue("2");
46. How do you type a typed event emitter?
Map event names to payload types in one interface, then constrain the event name to keyof that map. Callers get autocomplete for event names and the right payload type for each.
type Events = { login: { userId: string }; logout: undefined };
class Emitter<E extends Record<string, unknown>> {
private handlers: { [K in keyof E]?: Array<(p: E[K]) => void> } = {};
on<K extends keyof E>(event: K, fn: (p: E[K]) => void) {
(this.handlers[event] ??= []).push(fn);
}
emit<K extends keyof E>(event: K, payload: E[K]) {
this.handlers[event]?.forEach((fn) => fn(payload));
}
}
const bus = new Emitter<Events>();
bus.on("login", (p) => console.log(p.userId));
// @ts-expect-error payload must be { userId: string }
bus.emit("login", { id: "1" });
Practice: Event Emitter
47. What are const type parameters?
Added in TypeScript 5.0, <const T> makes inference treat the argument as if it had as const, so literals and tuples are kept instead of widened. Callers no longer have to remember to write as const themselves.
function routes<const T extends readonly string[]>(paths: T) {
return paths;
}
const r = routes(["/home", "/about"]); // readonly ["/home", "/about"]
48. What is NoInfer?
NoInfer<T> (TypeScript 5.4) blocks a position from being used as an inference site. It fixes cases where a default or fallback argument widens the type parameter, so the type is inferred only from the argument you meant.
// TypeScript 5.4+
declare function createLight<C extends string>(colors: C[], initial?: NoInfer<C>): void;
createLight(["red", "yellow", "green"], "red"); // OK
createLight(["red", "yellow", "green"], "blue"); // Error: "blue" is not one of the colors
49. When should you not use a generic?
When the type parameter appears only once. A generic should relate two things: an input to an output, or two inputs to each other. function log<T>(x: T): void adds nothing over (x: unknown) => void. The handbook’s guidelines: push type parameters down, use as few as possible, and if a type parameter appears in one place only, reconsider it.
function bad<T extends string>(s: T): void { console.log(s); } // T is pointless
function good(s: string): void { console.log(s); }
50. What is the difference between method and property syntax in an interface?
handle(e: Event): void (method syntax) and handle: (e: Event) => void (property syntax) look the same but are checked differently. With strictFunctionTypes, property syntax checks parameters contravariantly (sound), while method syntax stays bivariant for backward compatibility. Prefer property syntax when you want the stricter check.
interface A { handle: (e: MouseEvent) => void }
interface B { handle(e: MouseEvent): void }
const onAny = (e: Event) => {};
const onMouse = (e: MouseEvent) => {};
const a: A = { handle: onAny }; // OK: Event handler accepts MouseEvent
const b: B = { handle: onMouse };
const narrow = (e: MouseEvent) => {};
interface P { handle: (e: Event) => void }
interface M { handle(e: Event): void }
// @ts-expect-error property syntax is checked strictly
const p: P = { handle: narrow };
const m: M = { handle: narrow }; // method syntax: allowed (bivariant)
Utility Types

Find in-depth explanations and practice here: Typed Env Config ->
Utility types are the standard library of the type system. Interviewers expect you to know the common ones by heart and, for TypeScript interview questions for experienced developers, to explain how they’re built (the type challenges section implements several).
51. What do Partial and Required do?
Partial<T> makes every property optional; Required<T> makes every property required. Both are shallow. Partial is the usual type for update payloads and patch functions.
interface User { id: string; name: string; email?: string }
function updateUser(id: string, patch: Partial<User>) {}
updateUser("1", { name: "Ana" });
const full: Required<User> = { id: "1", name: "Ana", email: "a@x.dev" };
52. What is Readonly?
Readonly<T> marks every property readonly. It’s shallow and compile-time only: nested objects stay mutable, and Object.freeze is the runtime equivalent (also shallow). For deep immutability you write a recursive type, see Q110.
const state: Readonly<{ user: { name: string } }> = { user: { name: "Ana" } };
// @ts-expect-error user is read-only
state.user = { name: "Bo" };
state.user.name = "Bo"; // allowed: Readonly is shallow
53. What is the difference between Pick and Omit?
Pick<T, K> keeps only the keys K; Omit<T, K> keeps everything except K. One subtle difference: Pick constrains K to keyof T, so typos fail, but Omit accepts any key, so Omit<User, "nmae"> silently omits nothing.
interface User { id: string; name: string; password: string }
type PublicUser = Omit<User, "password">; // { id; name }
type Credentials = Pick<User, "id" | "password">;
type Typo = Omit<User, "pasword">; // no error, nothing omitted
54. What is Record, and how does it differ from an index signature?
Record<K, V> builds an object type with keys K and values V. With a union of literal keys it requires every key, which an index signature can’t do. Record<string, V> is equivalent to { [key: string]: V }.
type Theme = "light" | "dark";
const bg: Record<Theme, string> = { light: "#fff", dark: "#000" };
// @ts-expect-error property 'dark' is missing
const partialBg: Record<Theme, string> = { light: "#fff" };
55. What do Exclude and Extract do?
They filter unions. Exclude<T, U> removes members of T assignable to U; Extract<T, U> keeps only those. Both are distributive conditional types. Don’t confuse them with Omit, which removes object keys, not union members.
type Events = "click" | "scroll" | "keydown" | "keyup";
type Keyboard = Extract<Events, `key${string}`>; // "keydown" | "keyup"
type NonKeyboard = Exclude<Events, `key${string}`>; // "click" | "scroll"
56. What does NonNullable do?
NonNullable<T> removes null and undefined from T. Since TypeScript 4.8 it’s defined as T & {}, because {} means “any value except null or undefined”.
type MaybeName = string | null | undefined;
type Name = NonNullable<MaybeName>; // string
57. What are ReturnType and Parameters?
ReturnType<F> extracts a function type’s return type, and Parameters<F> extracts its parameters as a tuple. They pass a type, so for a function value you write typeof fn. They’re how you type wrappers around third-party functions whose types aren’t exported.
function createStore(initial: { count: number }, name?: string) {
return { state: initial, name };
}
type Store = ReturnType<typeof createStore>; // { state: { count: number }; name: string | undefined }
type Args = Parameters<typeof createStore>; // [initial: { count: number }, name?: string]
58. What does Awaited do?
Awaited<T> recursively unwraps promises the way await does, including nested promises and thenables. Combine it with ReturnType to get what an async function resolves to.
async function fetchUser() {
return { id: "1", name: "Ana" };
}
type User = Awaited<ReturnType<typeof fetchUser>>; // { id: string; name: string }
type Deep = Awaited<Promise<Promise<number>>>; // number
59. What are ConstructorParameters and InstanceType?
The class versions of Parameters and ReturnType: they take a constructor type (typeof MyClass) and give you the constructor’s argument tuple or the instance type. They’re useful in factories and dependency injection helpers.
class Client { constructor(public baseUrl: string, public timeout = 5000) {} }
type ClientArgs = ConstructorParameters<typeof Client>; // [baseUrl: string, timeout?: number]
type ClientInstance = InstanceType<typeof Client>; // Client
60. What do the string manipulation utility types do?
Uppercase, Lowercase, Capitalize, and Uncapitalize transform string literal types. They’re intrinsic (built into the compiler) and are mostly used inside template literal and mapped types, for example to generate onClick from click.
type Handler<E extends string> = `on${Capitalize<E>}`;
type H = Handler<"click" | "focus">; // "onClick" | "onFocus"
61. What is keyof?
keyof T produces a union of T‘s property names as literal types. For a type with a string index signature it gives string | number, since numeric keys are converted to strings in JavaScript.
interface User { id: string; age: number }
type UserKey = keyof User; // "id" | "age"
type Dict = { [k: string]: boolean };
type DictKey = keyof Dict; // string | number
62. What is the difference between typeof in a type position and in JavaScript?
In an expression, typeof x is the JavaScript operator and returns a string at runtime. In a type position, typeof x asks the compiler for the type of a value. Combined with keyof, it’s how you derive types from config objects instead of writing them twice.
const config = { api: "/api", retries: 3 };
type Config = typeof config; // { api: string; retries: number }
type ConfigKey = keyof typeof config; // "api" | "retries"
console.log(typeof config); // "object" (runtime)
63. What are indexed access types?
T["key"] looks up a property’s type, just like value indexing. You can index with a union of keys, and T[number] gives the element type of an array or tuple, which turns an as const array into a union.
interface Order { id: string; items: { sku: string; qty: number }[] }
type Item = Order["items"][number]; // { sku: string; qty: number }
const ROLES = ["admin", "editor", "viewer"] as const;
type Role = (typeof ROLES)[number]; // "admin" | "editor" | "viewer"
64. How do you derive types from a single source of truth?
Write the runtime value once and derive the types from it with typeof, keyof, indexed access, and utility types. Then adding a route, role, or feature flag updates every dependent type automatically. Interviewers ask this to see whether you avoid duplicated, drifting type definitions.
const FLAGS = {
newCheckout: false,
darkMode: true,
} as const;
type FlagName = keyof typeof FLAGS; // "newCheckout" | "darkMode"
function isOn(flag: FlagName): boolean {
return FLAGS[flag];
}
Advanced TypeScript Interview Questions

Find in-depth explanations and practice here: Precise typeof ->
These are the advanced TypeScript interview questions that come up for senior and staff roles, and in any round where you own a shared library or design system. The bar is explaining the behavior precisely, including edge cases like distribution over never.
65. What are conditional types?
A conditional type picks one of two types based on assignability: T extends U ? X : Y. It’s the type-level if. On its own it’s simple; the power comes from combining it with generics, infer, and recursion.
type IsString<T> = T extends string ? true : false;
type A = IsString<"hi">; // true
type B = IsString<42>; // false
66. What are distributive conditional types?
When the checked type is a naked type parameter and you pass a union, the conditional runs once per member and the results are unioned. That’s how Exclude works. Wrap both sides in a tuple to turn distribution off. A related edge case: never is the empty union, so a distributive conditional over never returns never.
type ToArray<T> = T extends unknown ? T[] : never;
type D = ToArray<string | number>; // string[] | number[]
type ToArrayND<T> = [T] extends [unknown] ? T[] : never;
type N = ToArrayND<string | number>; // (string | number)[]
type IsNever<T> = [T] extends [never] ? true : false;
type Z = IsNever<never>; // true (the naive T extends never version gives never)
67. What does infer do?
infer X declares a type variable inside the extends clause of a conditional type and captures whatever type sits in that position. It’s how ReturnType, Parameters, and Awaited are built. You can add a constraint (infer X extends string, TypeScript 4.7+).
type ElementOf<T> = T extends readonly (infer E)[] ? E : never;
type E1 = ElementOf<string[]>; // string
type MyReturnType<F> = F extends (...args: any[]) => infer R ? R : never;
type R1 = MyReturnType<() => Promise<number>>; // Promise<number>
type FirstArg<F> = F extends (first: infer A, ...rest: any[]) => any ? A : never;
type A1 = FirstArg<(id: string, n: number) => void>; // string
68. What are mapped types?
A mapped type builds a new object type by iterating over keys: { [K in Keys]: ... }. Most object utility types are mapped types. Modifiers can add or remove readonly and ? with + and -.
type Nullable<T> = { [K in keyof T]: T[K] | null };
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
type Concrete<T> = { [K in keyof T]-?: T[K] };
type U = Nullable<{ id: string }>; // { id: string | null }
69. What is key remapping with as in mapped types?
Since TypeScript 4.1, a mapped type can rename keys with an as clause. Remapping a key to never drops it, which is how you filter properties by their value type.
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type G = Getters<{ name: string; age: number }>;
// { getName: () => string; getAge: () => number }
type PickByValue<T, V> = { [K in keyof T as T[K] extends V ? K : never]: T[K] };
type Strings = PickByValue<{ a: string; b: number; c: string }, string>; // { a; c }
70. What are template literal types?
Template literal types build string literal types from other types, with the same syntax as template strings. With unions they produce every combination, and with infer they can parse strings at the type level, like route parameters.
type Size = "sm" | "lg";
type Color = "red" | "blue";
type ClassName = `${Size}-${Color}`; // "sm-red" | "sm-blue" | "lg-red" | "lg-blue"
type Params<Path> = Path extends `${string}:${infer P}/${infer Rest}`
? P | Params<`/${Rest}`>
: Path extends `${string}:${infer P}` ? P : never;
type P1 = Params<"/users/:userId/posts/:postId">; // "userId" | "postId"
71. What does the satisfies operator do?
satisfies (TypeScript 4.9) checks that a value matches a type without changing the value’s inferred type. A type annotation widens the value to the annotated type; satisfies validates and keeps the precise type, so you get both typo checking and specific autocomplete.
type Colors = Record<string, string | [number, number, number]>;
const annotated: Colors = { red: [255, 0, 0], green: "#0f0" };
// @ts-expect-error annotated.green is string | [number, number, number]
annotated.green.toUpperCase();
const checked = { red: [255, 0, 0], green: "#0f0" } satisfies Colors;
checked.green.toUpperCase(); // OK: still string
checked.red[0]; // OK: still a tuple
| Syntax | Checks the value | Resulting type |
|---|---|---|
const x: T = v |
Yes | T (widened) |
const x = v as T |
Barely | T (trusted) |
const x = v satisfies T |
Yes | Inferred type of v |
72. What does as const do?
A const assertion makes the compiler infer the narrowest type: literals stay literal, arrays become readonly tuples, and object properties become readonly. It has no runtime effect, so it doesn’t freeze the object. as const satisfies T is a common pairing for config objects.
const a = ["GET", "POST"]; // string[]
const b = ["GET", "POST"] as const; // readonly ["GET", "POST"]
const routes = { home: "/", about: "/about" } as const;
type Path = (typeof routes)[keyof typeof routes]; // "/" | "/about"
73. What are recursive types?
A type alias can refer to itself, which models trees, nested JSON, and deep transforms. TypeScript limits instantiation depth (you’ll see “Type instantiation is excessively deep and possibly infinite”), so recursive types over long tuples or strings need care.
type Json = string | number | boolean | null | Json[] | { [key: string]: Json };
interface TreeNode<T> {
value: T;
children: TreeNode<T>[];
}
const data: Json = { a: [1, "two", { b: null }] };
74. What is variance in TypeScript?
Variance describes how subtyping of a type argument carries to the generic type. If Dog is assignable to Animal:
| Variance | Meaning | Example |
|---|---|---|
| Covariant | F<Dog> to F<Animal> |
Return types, readonly arrays |
| Contravariant | F<Animal> to F<Dog> |
Function parameters (with strictFunctionTypes) |
| Invariant | Neither | A type that both reads and writes T |
| Bivariant | Both | Method parameters (for compatibility) |
The unsound spot interviewers love: mutable arrays are treated as covariant, so Dog[] is assignable to Animal[] and you can then push a Cat into a list of dogs without an error.
interface Animal { name: string }
interface Dog extends Animal { bark(): void }
const dogs: Dog[] = [];
const animals: Animal[] = dogs; // allowed (covariant arrays)
animals.push({ name: "cat" }); // compiles; dogs now holds a non-dog
const feedAnimal = (a: Animal) => {};
const feedDog: (d: Dog) => void = feedAnimal; // OK: contravariant parameter
75. What are the in and out variance annotations?
TypeScript 4.7 added optional in (contravariant), out (covariant), and in out (invariant) modifiers on type parameters. The compiler normally measures variance itself; annotations document intent, catch mistakes in the type’s definition, and can speed up checking for very large types. Most application code never needs them.
interface Producer<out T> { get(): T }
interface Consumer<in T> { accept(value: T): void }
interface Box<in out T> { get(): T; set(value: T): void }
76. What is declaration merging?
When two declarations share a name, TypeScript merges them. Interfaces with the same name combine their members. Namespaces merge with classes, functions, and enums to add static members. Type aliases never merge, which is the key type vs interface difference.
interface Settings { theme: string }
interface Settings { fontSize: number }
const s: Settings = { theme: "dark", fontSize: 14 }; // both required
function greet(name: string) { return `Hi ${greet.prefix}${name}`; }
namespace greet { export const prefix = "Dr. "; }
77. What is module augmentation?
Module augmentation uses declare module "pkg" inside a module file to add members to a library’s existing interfaces through declaration merging. It’s how you add a user field to Express’s Request, extend a theme type in a styling library, or type custom properties on a router’s metadata. You can add to interfaces; you can’t add new top-level exports this way.
// types/express.d.ts
import "express";
declare module "express-serve-static-core" {
interface Request {
user?: { id: string; role: "admin" | "member" };
}
}
78. How do you extend global types like Window?
Inside a module file, wrap the declaration in declare global and merge into the existing interface. In a non-module script file, top-level declarations are already global. Remember the runtime part: the type only claims the property exists, so something still has to set it.
export {};
declare global {
interface Window {
analytics?: { track(event: string): void };
}
}
window.analytics?.track("page_view");
79. What is the difference between {}, object, and Object?
| Type | Accepts | Rejects |
|---|---|---|
{} |
Any value except null and undefined, primitives included |
null, undefined |
object |
Non-primitives: objects, arrays, functions | Primitives, null, undefined |
Object |
Roughly the same as {}, plus checks against Object.prototype members |
null, undefined |
For “any object”, use object or Record<string, unknown>. For “anything”, use unknown.
80. What are branded (nominal) types?
A branded type adds a phantom property to a primitive so structurally identical values can’t be mixed: a UserId isn’t accepted where an OrderId is expected, even though both are strings. The brand exists only in the type system; a validation function is the only place that creates branded values.
type Brand<T, B extends string> = T & { readonly __brand: B };
type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
const toUserId = (s: string): UserId => s as UserId;
function getUser(id: UserId) {}
getUser(toUserId("u_1"));
// @ts-expect-error OrderId is not assignable to UserId
getUser("o_1" as OrderId);
81. What is the unique symbol type?
unique symbol is a subtype of symbol tied to one specific const declaration. It lets you use symbols as object keys with known property types, and it’s a tighter way to build brands than a string property, since no other code can produce the same key.
declare const emailBrand: unique symbol;
type Email = string & { [emailBrand]: true };
const KEY: unique symbol = Symbol("key");
const obj = { [KEY]: 42 };
const v = obj[KEY]; // number
82. What are abstract classes, and how do they differ from interfaces?
An abstract class can’t be instantiated and can mix implemented methods with abstract ones that subclasses must implement. It exists at runtime. An interface is erased and only describes a shape. Use an interface for contracts; use an abstract class when subclasses share real implementation.
abstract class Shape {
abstract area(): number;
describe() { return `Area: ${this.area().toFixed(1)}`; }
}
class Circle extends Shape {
constructor(private r: number) { super(); }
area() { return Math.PI * this.r ** 2; }
}
// @ts-expect-error cannot create an instance of an abstract class
new Shape();
83. What is the difference between private and #private?
TypeScript’s private is compile-time only: the property is a normal property at runtime and obj["secret"] still reaches it. JavaScript’s #private fields are enforced by the runtime, invisible to Object.keys and bracket access. Use # when privacy has to hold at runtime.
class Account {
private balance = 0;
#pin = "1234";
}
const acc = new Account();
console.log(acc["balance"]); // 0: bracket access bypasses TS private
84. What are decorators in TypeScript?
Decorators are functions applied to classes and class members with @ syntax. TypeScript 5.0 implemented the standard ECMAScript decorators proposal. The older experimentalDecorators flag still exists for Angular, NestJS, and other frameworks built on the legacy version, and the two aren’t compatible: the standard version doesn’t support parameter decorators or emitDecoratorMetadata.
function logged<This, Args extends unknown[], R>(
target: (this: This, ...args: Args) => R,
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => R>,
) {
return function (this: This, ...args: Args): R {
console.log(`calling ${String(context.name)}`);
return target.call(this, ...args);
};
}
class Api {
@logged
load(id: string) { return id; }
}
Modules, Declaration Files, and tsconfig

Find in-depth explanations and practice here: “use strict” ->
Tooling questions show up more in TypeScript interview questions for experienced engineers, because seniors are expected to set up a project, publish a library, or migrate a codebase without breaking the build.
85. What does strict enable, and what’s the default now?
strict turns on a family of checks: strictNullChecks, noImplicitAny, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, noImplicitThis, useUnknownInCatchVariables, alwaysStrict, and strictBuiltinIteratorReturn. New flags added to the family turn on automatically for projects using strict. Since TypeScript 6.0, strict defaults to true, so older projects that relied on the old default need "strict": false to keep their behavior.
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true
}
}
86. Which useful checks are not part of strict?
noUncheckedIndexedAccess: index and record access returnsT | undefined.exactOptionalPropertyTypes:prop?: stringmeans “missing”, and explicitly assigningundefinedis an error.noImplicitOverride: subclass methods that override must use theoverridekeyword.noFallthroughCasesInSwitch,noImplicitReturns,noUnusedLocals,noUnusedParameters.
A strong answer names noUncheckedIndexedAccess, since array and record lookups are the most common source of undefined crashes that strict still misses.
const list: string[] = [];
const first = list[0]; // string by default, string | undefined with noUncheckedIndexedAccess
87. What is a declaration file (.d.ts)?
A .d.ts file contains only types: no implementations. It describes the shape of JavaScript code so TypeScript can type-check against it. Libraries ship them next to their JavaScript, the declaration option generates them from your source, and you write them by hand for untyped code.
// math.d.ts
export declare function clamp(value: number, min: number, max: number): number;
export declare const VERSION: string;
88. What is DefinitelyTyped, and what are @types packages?
DefinitelyTyped is the community repository of type declarations for JavaScript libraries that don’t ship their own. It publishes them to npm under the @types scope, like @types/node. Libraries written in TypeScript include their own types and don’t need one. Since TypeScript 6.0, the types option defaults to [], so global @types packages like @types/node must be listed in types explicitly.
89. How do you type an untyped module or a non-code import?
Write an ambient module declaration. A shorthand declare module "lib"; types every import from it as any, which unblocks you quickly. For asset imports that bundlers handle, a wildcard declaration types them all.
// global.d.ts
declare module "legacy-lib";
declare module "*.svg" {
const src: string;
export default src;
}
declare module "*.module.css" {
const classes: Record<string, string>;
export default classes;
}
90. What does declare mean?
declare tells the compiler that something exists at runtime without emitting any code for it: a global variable injected by a script tag, a constant defined by the bundler, or a module. If nothing actually provides the value, you get a runtime ReferenceError.
declare const __APP_VERSION__: string; // injected by the bundler
console.log(__APP_VERSION__);
91. What is the difference between import and import type?
import type imports only types and is always erased from the output. A regular import that’s only used for types may be erased too, but single-file compilers can’t always tell. verbatimModuleSyntax (TypeScript 5.0) makes the rule simple: imports without type stay in the output, and imports marked type are dropped.
import type { ReactNode } from "react";
import { useState, type Dispatch } from "react"; // inline type modifier
92. What do isolatedModules and erasableSyntaxOnly do?
isolatedModules errors on code that a one-file-at-a-time compiler (Babel, esbuild, SWC) can’t transform correctly, like re-exporting a type without export type. erasableSyntaxOnly (TypeScript 5.8) goes further and errors on any TypeScript syntax that emits runtime code: enums, namespaces with values, and constructor parameter properties. That matches what Node.js type stripping supports, so the file runs by just deleting the types.
// Flagged by erasableSyntaxOnly:
enum Mode { A, B }
class Point { constructor(public x: number) {} }
93. What do module, moduleResolution, and target control?
| Option | Controls | Typical value |
|---|---|---|
target |
Which JavaScript syntax is emitted and which built-in types are assumed | es2022 or newer |
module |
Output module format | esnext (bundlers) or nodenext (Node) |
moduleResolution |
How import paths are resolved to files | bundler or nodenext |
lib |
Built-in type declarations, like dom |
Matches the runtime |
target doesn’t add polyfills: targeting an old version only down-levels syntax, so missing runtime APIs still need a polyfill.
94. What are path aliases, and what’s the catch?
paths in tsconfig maps import specifiers like @/components/* to folders. The catch is that TypeScript only uses it for type checking and doesn’t rewrite imports in the output, so your bundler or runtime needs the same alias configured. Since TypeScript 6.0 deprecated baseUrl, paths entries are written relative to the tsconfig file.
{
"compilerOptions": {
"paths": { "@/*": ["./src/*"] }
}
}
95. What are project references?
Project references split a large codebase or monorepo into smaller TypeScript projects that build in dependency order. Each referenced project sets composite: true, and tsc --build only rebuilds projects that changed, using their emitted .d.ts files instead of re-checking their source. The result is faster builds and enforced boundaries between packages.
{
"references": [{ "path": "./packages/ui" }, { "path": "./packages/api" }]
}
96. How would you migrate a large JavaScript codebase to TypeScript?
Incrementally. Turn on allowJs (and optionally checkJs with JSDoc types) so JavaScript and TypeScript coexist, then convert leaf modules first (utilities, API types) and work inward. Start with strict off if you must, but turn flags on one at a time and track error counts so the codebase only moves forward. Replace any with unknown at boundaries, and add runtime validation where data enters.
“I’d type the API layer first, because every component downstream benefits from it, and I’d add a lint rule against new
anyso we don’t add debt while paying it down.”
React TypeScript Interview Questions

Find in-depth explanations and practice here: React Coding Questions (solve them in TypeScript) ->
Most frontend TypeScript interviews happen inside React. These React TypeScript interview questions cover what you’ll type every day: props, children, events, hooks, refs, context, and generic components.
97. How do you type component props?
Declare a props type and annotate the destructured parameter. Most teams have moved away from React.FC for plain function components: annotating props directly is shorter, works the same with generics, and React.FC no longer adds anything you need since the React 18 type definitions removed its implicit children.
type ButtonProps = {
label: string;
variant?: "primary" | "ghost";
onClick: () => void;
};
export function Button({ label, variant = "primary", onClick }: ButtonProps) {
return <button className={variant} onClick={onClick}>{label}</button>;
}
98. How do you type children?
Use React.ReactNode: it covers elements, strings, numbers, arrays, fragments, null, and booleans. JSX.Element (or ReactElement) is narrower: a single element, no strings. PropsWithChildren<P> adds an optional children: ReactNode to a props type.
import type { ReactNode, PropsWithChildren } from "react";
function Card({ title, children }: { title: string; children: ReactNode }) {
return <section><h2>{title}</h2>{children}</section>;
}
function Layout({ children }: PropsWithChildren) {
return <main>{children}</main>;
}
99. How do you type event handlers?
React wraps native events, so use React’s types: React.ChangeEvent<HTMLInputElement>, React.MouseEvent<HTMLButtonElement>, React.FormEvent<HTMLFormElement>, and React.KeyboardEvent. Inline handlers get these types for free through contextual typing; you only write them when the handler is defined separately.
import { useState, type ChangeEvent, type FormEvent } from "react";
function Search() {
const [q, setQ] = useState("");
const onChange = (e: ChangeEvent<HTMLInputElement>) => setQ(e.target.value);
const onSubmit = (e: FormEvent<HTMLFormElement>) => e.preventDefault();
return (
<form onSubmit={onSubmit}>
<input value={q} onChange={onChange} />
</form>
);
}
100. How do you type useState?
Let inference handle simple initial values. Pass the type explicitly when the initial value doesn’t describe all future values: null before data loads, an empty array, or a union of states.
import { useState } from "react";
type User = { id: string; name: string };
function Profile() {
const [count, setCount] = useState(0); // number
const [user, setUser] = useState<User | null>(null); // User | null
const [tags, setTags] = useState<string[]>([]); // not never[]
return <p>{user?.name ?? "Guest"} {count} {tags.length}</p>;
}
101. How do you type useRef?
For DOM refs, pass the element type and null: useRef<HTMLInputElement>(null). React sets .current, so you check for null before using it. For mutable values like timer IDs, give the value type, and the ref’s .current is writable.
import { useEffect, useRef } from "react";
function AutoFocus() {
const inputRef = useRef<HTMLInputElement>(null);
const timer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
useEffect(() => {
inputRef.current?.focus();
timer.current = setTimeout(() => {}, 1000);
return () => clearTimeout(timer.current);
}, []);
return <input ref={inputRef} />;
}
102. How do you type useReducer?
Type the state and model actions as a discriminated union. The switch narrows each action to its payload, and dispatch rejects actions that don’t exist.
import { useReducer } from "react";
type State = { count: number };
type Action = { type: "inc" } | { type: "add"; amount: number } | { type: "reset" };
function reducer(state: State, action: Action): State {
switch (action.type) {
case "inc": return { count: state.count + 1 };
case "add": return { count: state.count + action.amount };
case "reset": return { count: 0 };
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return <button onClick={() => dispatch({ type: "add", amount: 5 })}>{state.count}</button>;
}
103. How do you type context so consumers never get undefined?
Create the context with undefined as the default, and expose a custom hook that throws if it’s used outside the provider. Consumers then get the non-nullable value type and a clear error message instead of a silent default.
import { createContext, useContext, useState, type ReactNode } from "react";
type Theme = { mode: "light" | "dark"; toggle: () => void };
const ThemeContext = createContext<Theme | undefined>(undefined);
export function ThemeProvider({ children }: { children: ReactNode }) {
const [mode, setMode] = useState<Theme["mode"]>("light");
const toggle = () => setMode((m) => (m === "light" ? "dark" : "light"));
return <ThemeContext.Provider value={{ mode, toggle }}>{children}</ThemeContext.Provider>;
}
export function useTheme(): Theme {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error("useTheme must be used inside ThemeProvider");
return ctx;
}
Deep dive: React Context and the Provider Pattern
104. How do you write a generic component?
Add a type parameter to the function component. TypeScript infers it from the props, so a List of users gets typed renderItem callbacks without the caller writing any types. In a .tsx file, an arrow function needs <T,> to avoid being parsed as JSX.
import type { ReactNode, Key } from "react";
type ListProps<T> = {
items: T[];
getKey: (item: T) => Key;
renderItem: (item: T) => ReactNode;
};
function List<T>({ items, getKey, renderItem }: ListProps<T>) {
return <ul>{items.map((item) => <li key={getKey(item)}>{renderItem(item)}</li>)}</ul>;
}
const users = [{ id: 1, name: "Ana" }];
const el = <List items={users} getKey={(u) => u.id} renderItem={(u) => u.name} />;
105. How do you extend native element props?
Use React.ComponentProps<"button"> (or ComponentPropsWithoutRef) and add your own props. Your wrapper then accepts every valid button attribute, like type, disabled, and aria-*, without listing them by hand. Omit the native props you redefine.
import type { ComponentProps } from "react";
type IconButtonProps = Omit<ComponentProps<"button">, "children"> & {
icon: string;
label: string;
};
function IconButton({ icon, label, ...rest }: IconButtonProps) {
return <button aria-label={label} {...rest}>{icon}</button>;
}
const b = <IconButton icon="x" label="Close" type="button" disabled />;
106. How do you make props mutually exclusive?
Use a discriminated union, or a union where the other branch marks the forbidden prop as never. That way TypeScript rejects a component that receives both href and onClick, or a controlled input without its onChange.
type LinkProps = { href: string; onClick?: never };
type ActionProps = { onClick: () => void; href?: never };
type Props = { label: string } & (LinkProps | ActionProps);
function Clickable(props: Props) {
return props.href
? <a href={props.href}>{props.label}</a>
: <button onClick={props.onClick}>{props.label}</button>;
}
const ok = <Clickable label="Docs" href="/docs" />;
// @ts-expect-error can't pass both href and onClick
const bad = <Clickable label="Docs" href="/docs" onClick={() => {}} />;
107. How do you type a custom hook’s return value?
Return an object for anything with more than two values, since names survive refactors. If you return a tuple, add as const; without it TypeScript infers an array of a union, and destructuring loses the per-position types.
import { useState } from "react";
function useToggle(initial = false) {
const [on, setOn] = useState(initial);
const toggle = () => setOn((v) => !v);
return [on, toggle] as const; // readonly [boolean, () => void]
}
function Demo() {
const [open, toggleOpen] = useToggle();
return <button onClick={toggleOpen}>{open ? "Close" : "Open"}</button>;
}
108. How do refs work as props in React 19, and what about forwardRef?
In React 19, function components receive ref as a regular prop, so you type it in the props like anything else and forwardRef is no longer needed for new code. On React 18 and earlier you still wrap the component in forwardRef<HTMLInputElement, Props>, with the ref type first and the props type second.
import { forwardRef, type ComponentPropsWithoutRef } from "react";
// React 18 style
const TextInput = forwardRef<HTMLInputElement, ComponentPropsWithoutRef<"input">>(
function TextInput(props, ref) {
return <input ref={ref} {...props} />;
},
);
TypeScript Coding Interview Questions: Type Challenges

Find in-depth explanations and practice here: JavaScript Coding Questions (switch the editor to TypeScript) ->
TypeScript coding interview questions come in two flavors: write runtime code with good types (like the debounce, memoize, and event emitter answers above), or implement a type with no runtime code at all. These are the type-level ones. For each, try it yourself first, then compare. Every solution compiles under strict, and the tests use this helper:
type Equal<X, Y> =
(<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
type Expect<T extends true> = T;
A test like type t = Expect<Equal<A, B>> fails to compile unless A and B are exactly the same type.
109. Implement MyPick<T, K>.
Constrain K to the keys of T, then map over K (not keyof T) and look up each property’s type. Mapping over K is what drops the other keys. Because it’s a homomorphic-style mapping over keys of T, the readonly and ? modifiers are preserved like the built-in Pick.
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
type Expect<T extends true> = T;
type MyPick<T, K extends keyof T> = { [P in K]: T[P] };
interface Todo { title: string; description: string; done: boolean }
type t1 = Expect<Equal<MyPick<Todo, "title" | "done">, { title: string; done: boolean }>>;
// @ts-expect-error "invalid" is not a key of Todo
type t2 = MyPick<Todo, "invalid">;
110. Implement DeepReadonly<T>.
Recurse into object properties, but stop at functions: a function is an object too, and mapping over it would turn it into an empty object type. Arrays and tuples map correctly, because a homomorphic mapped type over an array produces a readonly array.
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
type Expect<T extends true> = T;
type DeepReadonly<T> = T extends (...args: any[]) => any
? T
: T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;
type Input = { a: { b: string[] }; fn: () => void };
type Expected = { readonly a: { readonly b: readonly string[] }; readonly fn: () => void };
type t1 = Expect<Equal<DeepReadonly<Input>, Expected>>;
111. Implement MyOmit<T, K>.
Map over keyof T and remap the excluded keys to never, which deletes them. Using key remapping (instead of Pick<T, Exclude<keyof T, K>>) keeps it a single mapped type, which preserves modifiers.
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
type Expect<T extends true> = T;
type MyOmit<T, K extends keyof any> = { [P in keyof T as P extends K ? never : P]: T[P] };
interface Todo { readonly title: string; description?: string; done: boolean }
type t1 = Expect<Equal<MyOmit<Todo, "done">, { readonly title: string; description?: string }>>;
112. Implement TupleToUnion<T> and TupleToObject<T>.
Indexing a tuple with number gives the union of its element types. For the object version, map over that union so each value becomes both the key and the value. Constrain the input to readonly arrays so as const tuples are accepted.
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
type Expect<T extends true> = T;
type TupleToUnion<T extends readonly unknown[]> = T[number];
type TupleToObject<T extends readonly PropertyKey[]> = { [V in T[number]]: V };
const sizes = ["sm", "md", "lg"] as const;
type t1 = Expect<Equal<TupleToUnion<typeof sizes>, "sm" | "md" | "lg">>;
type t2 = Expect<Equal<TupleToObject<typeof sizes>, { sm: "sm"; md: "md"; lg: "lg" }>>;
113. Implement First<T> and Last<T> for tuples.
Pattern-match the tuple with infer and a rest element. The empty-tuple case falls into the false branch and returns never, which a naive T[0] doesn’t handle: indexing an empty tuple is a compile error.
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
type Expect<T extends true> = T;
type First<T extends readonly unknown[]> = T extends readonly [infer F, ...unknown[]] ? F : never;
type Last<T extends readonly unknown[]> = T extends readonly [...unknown[], infer L] ? L : never;
type t1 = Expect<Equal<First<[1, 2, 3]>, 1>>;
type t2 = Expect<Equal<Last<[1, 2, 3]>, 3>>;
type t3 = Expect<Equal<First<[]>, never>>;
114. Implement MyAwaited<T>.
Unwrap recursively until the type isn’t a promise-like anymore. Matching on PromiseLike instead of Promise also handles custom thenables, which is how the built-in Awaited behaves.
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
type Expect<T extends true> = T;
type MyAwaited<T> = T extends PromiseLike<infer V> ? MyAwaited<V> : T;
type t1 = Expect<Equal<MyAwaited<Promise<string>>, string>>;
type t2 = Expect<Equal<MyAwaited<Promise<Promise<number>>>, number>>;
type t3 = Expect<Equal<MyAwaited<boolean>, boolean>>;
115. Implement DeepPartial<T>.
The same recursion as DeepReadonly, with ? instead of readonly. It’s a practical one: typing deep config overrides and test fixtures. Keep functions as they are, and let arrays map naturally so string[] stays an array.
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
type Expect<T extends true> = T;
type DeepPartial<T> = T extends (...args: any[]) => any
? T
: T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
type Config = { server: { port: number; host: string }; debug: boolean };
const override: DeepPartial<Config> = { server: { port: 8080 } };
type t1 = Expect<Equal<DeepPartial<{ a: { b: number } }>, { a?: { b?: number } }>>;
116. Implement Includes<T, U> for a tuple.
Walk the tuple recursively and compare each element with the strict Equal helper. Using U extends T[number] looks shorter but is wrong for cases like Includes<[boolean], true>, which should be false.
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
type Expect<T extends true> = T;
type Includes<T extends readonly unknown[], U> = T extends readonly [infer F, ...infer R]
? Equal<F, U> extends true ? true : Includes<R, U>
: false;
type t1 = Expect<Equal<Includes<["a", "b"], "b">, true>>;
type t2 = Expect<Equal<Includes<[boolean], true>, false>>;
117. Implement Flatten<T> for nested tuples.
Take the first element: if it’s an array, flatten it and spread the result; otherwise keep it. Then recurse on the rest. Variadic tuple spreads ([...A, ...B]) do the concatenation.
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
type Expect<T extends true> = T;
type Flatten<T extends unknown[]> = T extends [infer F, ...infer R]
? F extends unknown[]
? [...Flatten<F>, ...Flatten<R>]
: [F, ...Flatten<R>]
: [];
type t1 = Expect<Equal<Flatten<[1, [2, [3, [4]]], 5]>, [1, 2, 3, 4, 5]>>;
Practice the runtime version: Flatten I
118. Implement PathKeys<T> (dot-notation paths of an object).
For each key, produce the key itself, plus key.subpath for every path inside the value if the value is an object. Template literal types join the segments. This is how typed get(obj, "a.b.c") helpers and form libraries type field names.
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
type Expect<T extends true> = T;
type PathKeys<T> = T extends object
? { [K in keyof T & string]: K | `${K}.${PathKeys<T[K]>}` }[keyof T & string]
: never;
type User = { name: string; address: { city: string; geo: { lat: number } } };
type t1 = Expect<Equal<
PathKeys<User>,
"name" | "address" | "address.city" | "address.geo" | "address.geo.lat"
>>;
119. Implement CamelCase<S> for kebab-case strings.
Parse the string with template literal inference: split at the first -, capitalize what follows, and recurse. String types are processed left to right, one separator per step.
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
type Expect<T extends true> = T;
type CamelCase<S extends string> = S extends `${infer Head}-${infer Tail}`
? `${Head}${CamelCase<Capitalize<Tail>>}`
: S;
type t1 = Expect<Equal<CamelCase<"background-color">, "backgroundColor">>;
type t2 = Expect<Equal<CamelCase<"border-top-left-radius">, "borderTopLeftRadius">>;
120. Write a type-safe pick(obj, keys) function.
This one mixes runtime and type-level code, which is the most common format in real TypeScript coding interviews. The generic signature returns Pick<T, K>, so callers get exactly the selected keys. Inside the function, the empty object needs one assertion, because TypeScript can’t prove the loop fills every key.
function pick<T extends object, K extends keyof T>(obj: T, keys: readonly K[]): Pick<T, K> {
const out = {} as Pick<T, K>;
for (const key of keys) {
if (key in obj) out[key] = obj[key];
}
return out;
}
const user = { id: 1, name: "Ana", password: "secret" };
const safe = pick(user, ["id", "name"]); // { id: number; name: string }
console.log(safe); // { id: 1, name: "Ana" }
// @ts-expect-error "email" is not a key of user
pick(user, ["email"]);
Practice the reverse problem: Omit Nested Paths
How to Use This List
Don’t read 120 questions in one sitting. Pick the sections that match the role, answer each question out loud before reading the answer, and type every code sample into an editor with strict on. TypeScript sticks when you see the compiler errors yourself.
| Level | Focus on | Then practice |
|---|---|---|
| Junior | Basics, narrowing, React TypeScript questions | Type a form component with events and useState |
| Mid-level | Functions and generics, utility types, tsconfig | Typed debounce, memoize, and an event emitter |
| Senior / staff | Advanced types, declaration files, migration strategy | All 12 type challenges, then a typed API client with runtime validation |
TypeScript interview questions for experienced developers usually end in a design discussion: how you’d type a shared component library, an API layer, or a migration. Practice explaining the trade-off (how strict, where to validate at runtime, when an assertion is acceptable), not only the syntax.
Where to go next on FrontendLead
- Quiz Yourself: pick the TypeScript topic for a quick multiple-choice check.
- JavaScript coding questions and React coding questions: switch the editor to TypeScript and solve them with types.
- Typed Env Config: a coding problem built around typing configuration.
- JavaScript interview questions and React interview questions: the runtime half of every TypeScript round.
- Frontend interview questions: the full map of what frontend loops cover.
- Study plans: a day-by-day schedule, or build your own with the study plan tool.
For anything this list doesn’t cover, the TSConfig reference and the TypeScript release notes are the sources interviewers trust.
Related reading: 150+ Angular interview questions and answers · React frontend developer: skills, stack, and career guide
