Types & Interfaces • Narrowing • Generics • Utility Types • React & APIs • 2026

TypeScript Interview Questions

30 questions What each one tests, an answer frame, a spoken answer 34 min read

This page is for anyone facing a TypeScript round, for a front-end, back-end or full-stack role. Most interviews start with why TypeScript and the core types, move to narrowing, generics and the built-in utility types, then test mapped and conditional types, declaration files and strict settings. Front-end rounds usually add typing React props and API responses. Senior rounds bring a migration story and a judgement call about any and ts-ignore. Each question shows what the interviewer is really checking, the shape of a strong answer and a short answer you can say out loud. Practise saying them, then swap in your own stories.

Search all questions by round, difficulty and level, or save the ones you want to practise.

Type Basics 7 questions

Easy Technical round Fresher, Mid-level Practice question

1. Why would a team choose TypeScript over plain JavaScript, and what does it still not protect you from?

What the interviewer is really testing:
Whether you see TypeScript as a compile-time tool with real trade-offs, and know that its types vanish when the code runs.
Answer frame:

Wins: wrong property names, missing null checks and bad arguments show up before the code runs; safer refactors and better autocomplete.

Cost: a build step, a learning curve and type code to maintain.

Limit: types are erased on compile, so data from the network, users or files is not checked at runtime.

Sample spoken answer:

"TypeScript adds a type checker on top of JavaScript. The big win for a team is that a whole class of mistakes shows up in the editor instead of in production: a typo in a property name, passing a string where a number is expected, forgetting that something can be undefined. It also makes refactoring much safer, because when I rename a field the compiler points at every place that breaks. The honest limit is that all of this happens at compile time. The types are stripped out and what runs is plain JavaScript. So if an API sends back a different shape than my type says, TypeScript won't notice. For anything coming from outside the program I still need runtime checks. The cost is a build step and time spent writing types, which pays off most on bigger codebases and teams."

Red flag to avoid:

Claiming TypeScript makes code run faster or guarantees there will be no type errors at runtime.

They may ask next:
  • If types are erased at runtime, how do you check the shape of a JSON response?
  • When might plain JavaScript with JSDoc type comments be enough?
Say it in 60 seconds
Easy Technical round Fresher, Mid-level Practice question

2. What's the difference between a type alias and an interface, and how do you choose between them?

What the interviewer is really testing:
Whether you know the real differences, like declaration merging and unions, rather than saying they're the same or inventing differences.
Answer frame:

Overlap: both describe object shapes, both can be extended, and a class can implement either.

Interface only: declaration merging; two declarations with the same name combine.

Type only: unions, tuples, primitives, and mapped or conditional types.

Choice: follow one team convention for object shapes; use type when you need what only type can do.

Sample spoken answer:

"For plain object shapes they're nearly interchangeable. Both can describe a user object, both can be extended, an interface with extends and a type with an intersection, and a class can implement either. The real differences are at the edges. An interface can be declared twice and the two declarations merge, which is how libraries let you add fields to things like the global Window. A type alias can't be reopened, but it can name anything: a union like 'loading' | 'done', a tuple, a primitive, or a mapped or conditional type. An interface can only describe an object shape. In practice I follow the team's convention. A common one is interface for object shapes and public APIs, and type for unions and computed types. Being consistent matters more than which one wins."

Red flag to avoid:

Saying interfaces are for classes and types are for data, or that one is faster at runtime when neither exists at runtime.

They may ask next:
  • What happens if two interfaces with the same name are declared in the same scope?
  • How is extending an interface different from intersecting two types when a property conflicts?
Say it in 60 seconds
Medium Technical round Fresher, Mid-level Practice question

3. Explain any, unknown and never. When would you actually use each one?

What the interviewer is really testing:
Whether you understand the top and bottom types, and reach for unknown instead of any when a type is not known yet.
Answer frame:

any: switches checking off for that value, and the looseness spreads to everything it touches.

unknown: accepts any value, but you must narrow it before using it; the safe type for outside data.

never: a type with no values; the return type of a function that always throws, and the tool for exhaustive checks.

Sample spoken answer:

"All three sit at the edges of the type system. any means stop checking. I can call anything on it, assign it anywhere, and mistakes slip through. Worse, the any leaks into whatever I derive from it. unknown also accepts any value, but it's the safe version: I can't do anything with it until I narrow it with a typeof check, an instanceof or a type guard. That makes it the right type for parsed JSON, caught errors and anything else from outside. never is the other end, a type with no possible values. A function that always throws returns never, and if I narrow a union until nothing is left, what remains is never. I use that on purpose in a switch's default branch, assigning the value to a never variable, so the build fails if someone adds a new case and forgets to handle it."

Red flag to avoid:

Treating any and unknown as the same thing, or describing never as another name for void.

They may ask next:
  • Why is the error in a catch block typed as unknown under strict settings?
  • What type do you get if you intersect string and number?
Say it in 60 seconds
Easy Technical round Fresher, Mid-level Practice question

4. What are union and intersection types? Give an example of each from real code.

What the interviewer is really testing:
Whether you can read A | B and A & B correctly, including which properties you're allowed to use on each.
Answer frame:

Union (A | B): the value is one of these; without narrowing you can use only what every member shares.

Intersection (A & B): the value has everything from both; used to combine shapes.

Gotcha: intersecting conflicting property types leaves a type no value can satisfy.

Sample spoken answer:

"A union says the value is one of several types. A status typed as 'idle' | 'loading' | 'error' is a union, and so is a parameter that takes string | number. The catch is that on a union I can only use what every member has, so before calling toUpperCase on string | number I have to check it's a string. An intersection goes the other way: the value has all the properties of both types. I use it to combine shapes, like a User intersected with an object holding a permissions array for an admin view. The trap is conflicts. If one side says id is a string and the other says it's a number, id becomes never and no value can fit. The names feel backwards at first: a union of object types gives you fewer usable properties, an intersection gives you more."

Red flag to avoid:

Thinking a union of two object types lets you read every property from both without any check.

They may ask next:
  • Given a union of two object types, how do you safely read a property only one of them has?
  • When would you use an intersection instead of interface extends?
Say it in 60 seconds
Medium Technical round Mid-level, Senior Practice question

5. TypeScript is structurally typed. What does that mean, and when has it surprised you?

What the interviewer is really testing:
Whether you know compatibility is decided by shape rather than by name, and understand excess property checks and branding.
Answer frame:

Shape wins: any value with the required properties fits the type, whatever it was declared as.

Excess checks: extra properties are flagged only on fresh object literals, not on variables.

Branding: when two same-shaped types must stay apart, add a brand that exists only in the type system.

Sample spoken answer:

"Structural typing means TypeScript compares shapes, not names. If a function wants a Point with x and y, any object with a numeric x and y is accepted, even if it was declared as a different type and has extra fields. That's unlike Java or C#, where the declared class matters. It surprises people in two ways. First, excess property checks only fire on a fresh object literal. If I pass an object literal with an extra z field straight into the function, I get an error, but if I put that same object in a variable first and pass the variable, it's accepted. Second, two types with the same shape are interchangeable, so a user ID string and an order ID string can be mixed up. When that matters, I use a branded type, so the compiler keeps them apart."

Code:
type UserId = string & { readonly __brand: 'UserId' };
type OrderId = string & { readonly __brand: 'OrderId' };

const asUserId = (raw: string) => raw as UserId;
function loadUser(id: UserId) { /* ... */ }

const orderId = 'ord_42' as OrderId;
loadUser(asUserId('u_7')); // ok
// loadUser(orderId);      // error: OrderId is not assignable to UserId
Red flag to avoid:

Saying a value must be declared with the exact type name to be accepted, as in a nominally typed language.

They may ask next:
  • Why does TypeScript only run excess property checks on object literals?
  • Can an instance of one class be passed where a different class with the same public fields is expected?
Say it in 60 seconds
Easy Technical round Fresher, Mid-level Practice question

6. What does as const do, and how would you use it to derive a union type from an array of values?

What the interviewer is really testing:
Whether you understand literal types and readonly inference, a pattern that keeps a runtime list and its type in one place.
Answer frame:

Narrow: literals keep their exact values instead of widening to string or number.

Readonly: object properties become readonly and arrays become readonly tuples.

Derive: indexing the tuple's type with number turns it into a union, so the list and the type never drift apart.

Sample spoken answer:

"Normally TypeScript widens literals. If I write const roles equals an array of 'admin' and 'editor', the type is string[], because arrays can change. Adding as const tells the compiler to treat the whole thing as a fixed value: the array becomes a readonly tuple of exactly those strings, and object properties become readonly with their literal values, nested ones included. The pattern I use a lot is to write the list once as a runtime array with as const, then get the type by indexing typeof roles with number, which gives the union 'admin' | 'editor'. Now I can loop over the array to render a dropdown and use the type to check function arguments, and adding a role in one place updates both. It's a nice alternative to an enum. The one thing to remember is that I can't push to that array later."

Code:
const ROLES = ['admin', 'editor', 'viewer'] as const;
type Role = (typeof ROLES)[number]; // 'admin' | 'editor' | 'viewer'

function canEdit(role: Role): boolean {
  return role !== 'viewer';
}

ROLES.forEach((r) => console.log(r, canEdit(r)));
// canEdit('owner'); // error: not assignable to Role
Red flag to avoid:

Confusing as const with the const keyword, which only stops the variable from being reassigned.

They may ask next:
  • How is as const different from declaring the variable with the const keyword?
  • What error do you get if you call push on that array?
Say it in 60 seconds
Medium Technical round Mid-level, Senior Practice question

7. Should you use an enum or a union of string literals? What does each one compile to?

What the interviewer is really testing:
Whether you know enums are one of the few TypeScript features that emit runtime code, and the trade-offs that follow from that.
Answer frame:

Enum: becomes a real object at runtime; numeric enums also map values back to names.

Union: 'a' | 'b' disappears on compile; plain strings in and out, nothing added to the bundle.

Trade-offs: string enums reject plain strings; const enums don't fit tools that compile one file at a time.

Sample spoken answer:

"An enum is one of the few TypeScript features that isn't only types. It compiles to a real JavaScript object, and a numeric enum also gets a reverse mapping, so you can look up a name from its number. A union of string literals like 'draft' | 'published' is erased completely. My default is the union. The values are plain strings, so they match JSON from an API without converting, and nothing extra ships. With a string enum, even though the value is 'draft', I can't pass the plain string 'draft' where the enum is expected, which gets awkward at API boundaries. If I need the values at runtime, to loop over them, I pair the union with an as const array or object. Enums are fine if the team likes them, but I'd avoid const enums in projects built by tools that compile each file on its own, since those tools can't inline them across files."

Red flag to avoid:

Believing enums vanish on compile like other types, or that a string literal union can never be listed at runtime.

They may ask next:
  • What does a numeric enum's reverse mapping look like in the compiled output?
  • How would you get both a runtime list of values and a type from one declaration without an enum?
Say it in 60 seconds

Narrowing 4 questions

Easy Technical round Fresher, Mid-level Practice question

8. How does TypeScript narrow a union type inside an if statement? Walk me through the checks it understands.

What the interviewer is really testing:
Whether you understand control flow analysis and know each narrowing check along with its blind spots.
Answer frame:

typeof: for primitives; remember typeof null is 'object'.

instanceof and in: instanceof for class instances, in to check that a property exists.

Equality and truthiness: comparing to null or a literal narrows; truthiness also drops 0 and empty strings.

Flow: the compiler follows branches, early returns and assignments.

Sample spoken answer:

"TypeScript follows the flow of the code and narrows the type in each branch. With string | number, after if typeof x equals 'string', it knows x is a string inside the block, and a number after it if that block returns early. typeof works for primitives, with the known trap that typeof null is 'object'. For class instances I use instanceof, like checking whether an error is an HttpError. The in operator checks a property exists, so checking 'swim' in animal narrows to the types that have swim. Equality narrows too: comparing to null removes null, and comparing a status field to a literal picks one member of a discriminated union. Truthiness works but is risky, since if count also drops zero and if name drops an empty string. The compiler also tracks early returns and reassignments, so the order of my checks matters."

Red flag to avoid:

Checking typeof x === 'object' to find an object and forgetting that null passes that check too.

They may ask next:
  • Why can't you narrow with instanceof when the type is an interface?
  • Why can narrowing on a variable be lost inside a callback?
Say it in 60 seconds
Medium Coding round Mid-level, Senior Practice question

9. How do you write your own type guard? What's the risk with a function that returns 'value is User'?

What the interviewer is really testing:
Whether you can write a custom guard, and understand the compiler trusts it blindly, so a sloppy guard makes the types lie.
Answer frame:

Predicate: a function returning value is T narrows its argument wherever it returns true.

Trust: the compiler doesn't check the body; a wrong guard spreads a wrong type to every caller.

Assertions: asserts value is T throws instead of returning false, and narrows everything after the call.

Sample spoken answer:

"A type guard is a function whose return type is a predicate like value is User. When I call it in an if, the compiler narrows the argument to User inside that branch. That's how I turn unknown data into a typed value, and it also works with filter to drop nulls from an array. The risk is that TypeScript takes my word for it. It doesn't check that the body proves what the predicate claims. If my isUser only checks that the value is an object, every caller now believes there's a name and an email, and the bug shows up far away. So I keep guards small, check every field I rely on, and unit test them with bad inputs. There's also an assertion form, asserts value is User, which throws on bad data and narrows everything after the call. That's handy at the top of a request handler."

Code:
interface User { id: number; email: string }

function isUser(value: unknown): value is User {
  if (typeof value !== 'object' || value === null) return false;
  const v = value as Record<string, unknown>;
  return typeof v.id === 'number' && typeof v.email === 'string';
}

function assertUser(value: unknown): asserts value is User {
  if (!isUser(value)) throw new Error('Not a user');
}

const data: unknown = JSON.parse('{"id":1,"email":"a@b.co"}');
if (isUser(data)) console.log(data.email.toLowerCase());
Red flag to avoid:

Writing a guard that checks one field and claims the whole type, or casting with as instead of checking.

They may ask next:
  • How would you use a type guard with Array.filter to remove null values?
  • When would you pick an assertion function over a guard that returns a boolean?
Say it in 60 seconds
Medium Coding round Mid-level, Senior Practice question

10. What is a discriminated union, and how do you make the compiler force you to handle every case?

What the interviewer is really testing:
Whether you can model states so impossible combinations can't exist, and use never to make a switch exhaustive.
Answer frame:

Tag: every member shares a field like kind or status, each with a different literal value.

Switch: checking the tag narrows to exactly one member and its own fields.

Exhaustive: in the default branch, assign the value to never, so a new member breaks the build until it's handled.

Sample spoken answer:

"A discriminated union is a union of object types that all share one field with a literal value, like kind: 'circle' or kind: 'square'. When I switch on that field, TypeScript narrows to the exact member, so in the circle case I can read radius and it knows there's no side. I use it a lot for request state: idle, loading, success with data, error with a message. Then it's impossible to have data and an error at once, which a bag of optional fields would allow. To make it exhaustive, in the default branch I assign the value to a variable typed never. If every case is handled, nothing is left and that compiles. The day someone adds a triangle and forgets it, the triangle reaches the default, can't be assigned to never, and the build fails right where the fix belongs."

Code:
type Shape =
  | { kind: 'circle'; radius: number }
  | { kind: 'square'; side: number }
  | { kind: 'rect'; width: number; height: number };

function area(s: Shape): number {
  switch (s.kind) {
    case 'circle':
      return Math.PI * s.radius ** 2;
    case 'square':
      return s.side ** 2;
    case 'rect':
      return s.width * s.height;
    default: {
      const unhandled: never = s;
      throw new Error(`Unhandled shape: ${JSON.stringify(unhandled)}`);
    }
  }
}
Red flag to avoid:

Modelling state with several optional booleans and fields, so the type allows loading and error to be true together.

They may ask next:
  • Why is this better than one object with optional data and error fields?
  • How would you get the same exhaustive check without a switch statement?
Say it in 60 seconds
Medium Technical round Mid-level, Senior Practice question

11. What's the difference between a type annotation, an as assertion, the non-null ! operator and satisfies?

What the interviewer is really testing:
Whether you know which of these actually check your code and which only silence the compiler.
Answer frame:

Annotation: checks the value, then the variable has the declared type and loses the more specific inferred one.

as: asks the compiler to trust you; no runtime check, refused only when the types can't overlap.

Non-null !: removes null and undefined on your word alone.

satisfies: checks the value against a type but keeps the narrower inferred type.

Sample spoken answer:

"An annotation checks and then takes over: the value must fit the type, and from then on the variable has the declared type, so the exact keys TypeScript would have inferred are lost. An as assertion is me overruling the compiler. It checks nothing at runtime, and it's only refused when the two types can't possibly overlap, which is why people chain as unknown as something, a big warning sign. The non-null exclamation mark is a smaller version: it strips null and undefined because I say so, and if I'm wrong, it crashes at runtime. satisfies is newer and my favourite for config objects. It checks the value against a type, so typos and missing fields are caught, but the variable keeps its own inferred type. So a routes object that satisfies a Record of routes still gives me autocomplete on its real keys, which an annotation would throw away."

Code:
type Route = { path: string; auth: boolean };

const routes = {
  home: { path: '/', auth: false },
  admin: { path: '/admin', auth: true },
} satisfies Record<string, Route>;

routes.admin.path;      // ok, the real keys are known
// routes.settings;     // error: no such key

const annotated: Record<string, Route> = routes;
annotated.settings;     // compiles, but is undefined at runtime
Red flag to avoid:

Treating as like a conversion that changes the value, or scattering ! everywhere to quiet null errors.

They may ask next:
  • When is an as assertion actually the right tool?
  • Why is as unknown as SomeType a red flag in code review?
Say it in 60 seconds

Generics 2 questions

Easy Coding round Fresher, Mid-level Practice question

12. Why use generics instead of any? Show me a small generic function.

What the interviewer is really testing:
Whether you see generics as a way to keep the link between input and output types, not just a way to accept anything.
Answer frame:

Link types: a type parameter connects what goes in to what comes out.

any loses it: with any, the result is any and the caller loses all checking.

Inference: callers rarely write the type argument; it's worked out from the arguments.

Sample spoken answer:

"With any, a function can accept everything, but it throws the information away. If first takes an any array and returns any, the caller gets any back, and a mistake on the result goes unnoticed. A generic keeps the link. If first takes an array of T and returns T or undefined, then passing strings gives back a string or undefined, and passing users gives back a user. The type parameter is like a variable for types, filled in at each call, and callers almost never write it because TypeScript infers it from the argument. Generics are everywhere: arrays, promises, useState, an API helper that returns a promise of T. My test is simple: does the output type depend on the input type? If yes, it's a generic. If the function truly doesn't care about the type, unknown is often better than a type parameter."

Code:
function first<T>(items: T[]): T | undefined {
  return items[0];
}

const n = first([3, 1, 2]);   // number | undefined
const s = first(['a', 'b']);  // string | undefined

function firstAny(items: any[]): any {
  return items[0];
}
firstAny(['a']).toFixed(2);   // compiles, crashes at runtime
Red flag to avoid:

Describing generics as a fancier any, or adding a type parameter that is used only once.

They may ask next:
  • Why do people say a type parameter should appear at least twice in a signature?
  • How would you type a helper that fetches JSON and returns a promise of whatever the caller expects?
Say it in 60 seconds
Medium Coding round Mid-level, Senior Practice question

13. What are generic constraints? Write a function that safely reads a property of an object by its key.

What the interviewer is really testing:
Whether you can combine extends and keyof so the compiler checks the key and works out the exact return type.
Answer frame:

Constraint: T extends Something limits what T can be, so you can use Something's members inside.

keyof: K extends keyof T means only real keys of T are allowed.

Indexed result: returning T[K] gives the exact type of that property.

Sample spoken answer:

"A constraint limits what a type parameter can be. Without one, T could be anything inside a generic function, so I can't even read length on it. If I write T extends an object with a numeric length, I can, and callers can pass strings or arrays but not numbers. The classic example is a getProperty function. I take two type parameters, T for the object and K extends keyof T for the key. Now the compiler rejects a key that doesn't exist on the object, and the return type is T indexed by K, so getting 'age' from a user gives a number and getting 'name' gives a string. That's far better than returning any. Notice that a constraint can refer to another type parameter, which is exactly what K does here. I can also give a default, like T equals unknown, for when nothing is inferred."

Code:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}

const user = { name: 'Asha', age: 31 };
const age = getProperty(user, 'age');  // number
// getProperty(user, 'email');         // error: not a key of user
longest([1, 2], [1, 2, 3]);            // number[]
// longest(10, 20);                    // error: number has no length
Red flag to avoid:

Typing the key as a plain string and returning any, which loses both the key check and the result type.

They may ask next:
  • How would you type a function that picks several keys from an object and returns a smaller object?
  • What's the difference between a constraint and a default on a type parameter?
Say it in 60 seconds

Utility Types 2 questions

Easy Technical round Fresher, Mid-level Practice question

14. Walk me through Partial, Pick, Omit and Record. Where have you used each one?

What the interviewer is really testing:
Whether you derive types from one source of truth instead of copying shapes, and know each helper's quirks.
Answer frame:

Partial: every property optional; update payloads and patch functions.

Pick and Omit: keep or drop named keys; public or form views of a model.

Record: an object type from a set of keys and one value type; lookups and maps.

Quirk: Omit doesn't check that the keys you drop actually exist.

Sample spoken answer:

"They all build new types from existing ones, so I don't copy shapes by hand. Partial makes every property optional. I use it for an update function where the caller sends only the fields that changed. Pick keeps just the keys I name, like Pick of User with id and name for a list view. Omit is the reverse. A common one is omitting passwordHash for what goes to the client, or omitting id for a create form where the database makes the id. Record builds an object type from a set of keys and a value type, like a Record from Status to string for a label per status, and if Status is a union, it forces every key to be present. One quirk worth knowing: Omit accepts any key, even one that doesn't exist on the type, so a typo there silently does nothing."

Red flag to avoid:

Copying the User shape into three hand-written types that drift apart over time.

They may ask next:
  • Is there any difference between Record<string, number> and an object type with a string index signature?
  • Partial is shallow. How would you make nested properties optional too?
Say it in 60 seconds
Medium Coding round Mid-level, Senior Practice question

15. How would you write Partial and Readonly yourself using mapped types?

What the interviewer is really testing:
Whether you understand mapped types well enough to build your own helpers, including adding and removing modifiers.
Answer frame:

Loop over keys: [K in keyof T] visits every property, and T[K] is its type.

Modifiers: add ? or readonly; remove them with -? and -readonly.

Shallow: these only touch the top level; nested objects need recursion.

Sample spoken answer:

"A mapped type loops over the keys of another type and builds a new property for each one. The syntax is K in keyof T, mapped to T indexed by K, which on its own just copies T. To write Partial, I add a question mark after the key, so every property becomes optional. Readonly is the same idea with the readonly modifier in front. You can also remove modifiers with a minus sign: minus question mark makes everything required, which is how Required works, and minus readonly gives a Mutable helper that isn't built in. Two things I'd point out. These are shallow, so a nested address stays fully required inside a Partial user. For deep versions I'd recurse when the property is an object, taking care with arrays and functions. And because the mapping is over keyof T, the original optional and readonly flags carry across unless I change them."

Code:
type MyPartial<T> = { [K in keyof T]?: T[K] };
type MyReadonly<T> = { readonly [K in keyof T]: T[K] };
type MyRequired<T> = { [K in keyof T]-?: T[K] };
type Mutable<T> = { -readonly [K in keyof T]: T[K] };

interface Settings {
  readonly theme: string;
  fontSize?: number;
}

type Editable = Mutable<Settings>;  // theme is writable
type Full = MyRequired<Settings>;   // fontSize is required
Red flag to avoid:

Assuming the built-in Partial and Readonly reach into nested objects, when they only change the top level.

They may ask next:
  • How would you write a DeepPartial, and what goes wrong with arrays and functions?
  • How could you make only some keys optional and leave the rest unchanged?
Say it in 60 seconds

Advanced Types 3 questions

Medium Technical round Mid-level, Senior Practice question

16. Explain keyof, typeof in a type position, and indexed access types. How do they fit together?

What the interviewer is really testing:
Whether you can pull types out of existing values and types so there is one source of truth instead of two.
Answer frame:

typeof: in a type position, gives the type of a runtime value.

keyof: gives a union of an object type's keys.

Indexed access: T['key'] reads one property's type; T[keyof T] gives all value types.

Sample spoken answer:

"These three let me derive types instead of writing them twice. typeof in a type position takes a runtime value, say a config object, and gives me its type. keyof takes an object type and gives the union of its keys, so keyof Config might be 'port' | 'host' | 'debug'. Indexed access reads a property's type the way brackets read a value: Config of 'port' is number. Put together, Config indexed by keyof Config is the union of all the value types. A real case: I have a const object of API endpoints. I use typeof to get its type, keyof on that to get the valid endpoint names, and a function that only accepts one of those names. Now adding an endpoint to the object makes it valid everywhere without touching a type. Indexing an array type with number works the same way and gives the element type."

Code:
const config = { port: 3000, host: 'localhost', debug: false };

type Config = typeof config;          // { port: number; host: string; debug: boolean }
type ConfigKey = keyof Config;        // 'port' | 'host' | 'debug'
type Port = Config['port'];           // number
type ConfigValue = Config[ConfigKey]; // number | string | boolean

function read<K extends ConfigKey>(key: K): Config[K] {
  return config[key];
}
Red flag to avoid:

Mixing up typeof in a type position with the runtime typeof operator that returns strings like 'object'.

They may ask next:
  • What does keyof give you for a type with a string index signature?
  • How is typeof in a type position different from the typeof operator at runtime?
Say it in 60 seconds
Hard Coding round Mid-level, Senior Practice question

17. What is a conditional type, and how does infer work? Could you write your own ReturnType?

What the interviewer is really testing:
Whether you can read and write type-level logic, including how conditional types distribute over unions.
Answer frame:

Shape: T extends U ? X : Y picks a type based on whether T fits U.

infer: declares a type variable inside the check to capture part of T.

Distribution: on a bare type parameter, a union is split and each member is checked on its own.

Sample spoken answer:

"A conditional type is an if statement for types: T extends U, question mark, X, colon, Y. If T is assignable to U you get X, otherwise Y. infer lets me capture part of a type while matching. For ReturnType, I check whether T is a function type with a return type I call infer R, and if it is, the result is R, otherwise never. The same trick unwraps an array's element type or a promise's value. The subtle part is distribution. When the checked type is a bare type parameter and you pass in a union, the condition runs on each member separately and the results are joined. So ToArray of string | number gives string[] | number[], not one mixed array. That's usually what you want, it's how Exclude works, but when it isn't, wrapping both sides in square brackets turns distribution off."

Code:
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type ElementOf<T> = T extends readonly (infer U)[] ? U : never;

type A = MyReturnType<() => Promise<number>>; // Promise<number>
type B = ElementOf<string[]>;                 // string

type ToArray<T> = T extends unknown ? T[] : never;
type C = ToArray<string | number>;            // string[] | number[]

type ToArrayWhole<T> = [T] extends [unknown] ? T[] : never;
type D = ToArrayWhole<string | number>;       // (string | number)[]
Red flag to avoid:

Thinking conditional types run at runtime, or being surprised when a union input comes back as a union of results.

They may ask next:
  • How is the built-in Exclude written, and why does it depend on distribution?
  • How would you unwrap nested promises to get the final value type?
Say it in 60 seconds
Hard Coding round Senior Practice question

18. Using a mapped type with key remapping, build a type that turns an object's fields into getter methods like getName.

What the interviewer is really testing:
Whether you can combine mapped types, template literal types and the as clause, which typed libraries use heavily.
Answer frame:

Remap: an as clause inside a mapped type renames each key.

Template literal: a template literal type with the built-in Capitalize builds the new name from the old key.

Filter: remapping a key to never drops it from the result.

Sample spoken answer:

"I'd write a mapped type over keyof T with an as clause, which lets me rename each key as I go. The new name is a template literal type: the word get followed by the key with its first letter capitalised, using the built-in Capitalize helper. The value becomes a function with no arguments that returns T indexed by K. One detail: keyof T can include number and symbol keys, and Capitalize only takes strings, so I intersect K with string, which quietly drops the others. For a Person with name and age, I get getName returning string and getAge returning number. The same as clause is handy for filtering: if I remap a key to never, it disappears. That's how I'd build a type with only the data fields of an object and none of its methods."

Code:
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

interface Person { name: string; age: number }

type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number }

type DataOnly<T> = {
  [K in keyof T as T[K] extends (...args: any[]) => unknown ? never : K]: T[K];
};
Red flag to avoid:

Hand-writing a getter interface for every model, so it drifts the moment someone adds a field.

They may ask next:
  • How would you type the matching setters, like setName taking a string?
  • Where have you seen template literal types used in a real library or codebase?
Say it in 60 seconds

Config & Declarations 3 questions

Medium Technical round Fresher, Mid-level Practice question

19. What is a .d.ts file? What do you do when a package you install has no type definitions?

What the interviewer is really testing:
Whether you know where types for JavaScript libraries come from, and the sensible order of fixes when they're missing.
Answer frame:

What: a file with only type information and no runtime code, describing JavaScript to the compiler.

Where from: the package ships its own, or there's a community package under the @types scope.

Missing: look for @types first, then declare the parts you use; a bare declare module is the last resort.

Sample spoken answer:

"A .d.ts file is a declaration file. It holds only types, no implementation, and tells the compiler what a piece of JavaScript looks like. When I publish a TypeScript library, the compiler generates them with the declaration option, so the JavaScript ships alongside its types. When I install a package, types come from one of two places: the package includes its own, usually listed in its package.json, or there's a community package under the @types scope that I add as a dev dependency. If neither exists, I don't spread any across the codebase. I write a small declaration file with declare module and the package name, and describe just the functions I actually use. The quick escape hatch is a one-line declare module with no body, which types the whole package as any. That's fine for a spike, but I'd flag it to replace."

Red flag to avoid:

Putting ts-ignore above every import, or thinking a .d.ts file contains code that runs.

They may ask next:
  • How does the compiler find the types for a package you import?
  • What changes when you publish a library with the declaration option turned on?
Say it in 60 seconds
Hard Coding round Mid-level, Senior Practice question

20. How do you add a property to a type you don't own, like a field on the global Window or on a library's interface?

What the interviewer is really testing:
Whether you understand declaration merging and module augmentation, and the rules that make it quietly fail.
Answer frame:

Merging: interfaces with the same name combine, so you reopen the existing interface.

Global: in a module file, use declare global and reopen Window.

Library: declare module with the package name, then reopen the interface it exports.

Rules: the file must be a module and inside the project's include paths; type aliases never merge.

Sample spoken answer:

"It works through declaration merging. Interfaces with the same name merge, so I can reopen an interface someone else declared and add fields. For the global Window, I create a declaration file, make it a module by adding an empty export, and inside declare global I declare interface Window with the new property. For a library, it's module augmentation: declare module with the package's name, then reopen the interface it exports, like a request or theme type. What trips people up is usually the file not being picked up. If it isn't a module, declare global is an error. If it isn't inside the include paths in tsconfig, nothing happens and the property is just missing. And I have to reopen an interface, because type aliases don't merge. Augmentation can extend what's there, but it can't add brand new top-level declarations to someone else's module."

Code:
// types/global.d.ts
export {};

declare global {
  interface Window {
    analyticsQueue: unknown[];
  }
}

// anywhere in the app
window.analyticsQueue = window.analyticsQueue || [];
window.analyticsQueue.push({ event: 'page_view' });
Red flag to avoid:

Casting to any at every use, like (window as any).something, instead of declaring the property once.

They may ask next:
  • How would you add a user field to the request object of the web framework you use?
  • Your augmentation works in one project but not in another. What would you check first?
Say it in 60 seconds
Medium Technical round Mid-level, Senior Practice question

21. What does strict mode turn on in tsconfig, and which of those checks matters most to you?

What the interviewer is really testing:
Whether you know strict is a family of flags, which ones catch real bugs, and that some useful checks sit outside it.
Answer frame:

Family: strict enables a group, including noImplicitAny, strictNullChecks, strictFunctionTypes and strictPropertyInitialization.

Biggest win: strictNullChecks makes null and undefined separate types, so missing checks become errors.

Outside strict: noUncheckedIndexedAccess and exactOptionalPropertyTypes are separate opt-ins.

Sample spoken answer:

"strict isn't one check, it's a switch for a family of them. The main ones are noImplicitAny, which stops parameters quietly becoming any, strictNullChecks, strictFunctionTypes for safer checks on function parameters, strictPropertyInitialization, which makes class fields get a value where they're declared or in the constructor, and useUnknownInCatchVariables, which types caught errors as unknown. The one that matters most to me is strictNullChecks. Without it, null and undefined are allowed everywhere, so the compiler never warns that a find call might return nothing, and that's one of the most common crashes in JavaScript. With it on, I have to handle the missing case. New compiler versions can add flags to the strict family, so an upgrade can surface new errors. And some useful checks aren't included: noUncheckedIndexedAccess, which adds undefined when reading an array or record by index, is one I turn on in new projects."

Red flag to avoid:

Thinking strict is a single rule, or switching it off for the whole project to make the errors go away.

They may ask next:
  • What changes about reading items[0] when noUncheckedIndexedAccess is on?
  • Why doesn't strictFunctionTypes apply to methods written with method syntax?
Say it in 60 seconds

React & APIs 3 questions

Easy Technical round Fresher, Mid-level Practice question

22. How do you type a React component's props, including children, optional props and event handlers?

What the interviewer is really testing:
Whether you can type everyday components cleanly, which is most of the TypeScript a front-end role writes.
Answer frame:

Props type: a type or interface for props; optional props with ? and defaults in the destructuring.

Children: React.ReactNode for anything that can be rendered.

Events: React's own event types, like React.ChangeEvent<HTMLInputElement>.

Native props: extend React.ComponentProps<'button'> to accept everything a button accepts.

Sample spoken answer:

"I write a props type and annotate the destructured parameter with it. Optional props get a question mark, and I set defaults right in the destructuring, so variant defaults to primary and the rest of the component never sees undefined. For children I use React.ReactNode, which covers text, elements, arrays and null. For event handlers I use React's event types: an input's change handler gets a ChangeEvent of HTMLInputElement, so event.target.value is typed as a string. When I wrap a native element, like a custom Button, I extend the button's component props so callers can pass type, disabled and aria attributes without me listing them. I rarely use React.FC now. In current type definitions it no longer adds children for you, and a plain function with typed props is simpler. For state, useState infers from the initial value, and I pass a type when it starts as null."

Code:
import * as React from 'react';

type ButtonProps = React.ComponentProps<'button'> & {
  variant?: 'primary' | 'secondary';
  icon?: React.ReactNode;
};

function Button({ variant = 'primary', icon, children, ...rest }: ButtonProps) {
  return (
    <button className={`btn btn-${variant}`} {...rest}>
      {icon}
      {children}
    </button>
  );
}

function SearchBox({ onSearch }: { onSearch: (q: string) => void }) {
  const [q, setQ] = React.useState('');
  const onChange = (e: React.ChangeEvent<HTMLInputElement>) => setQ(e.target.value);
  return <input value={q} onChange={onChange} onBlur={() => onSearch(q)} />;
}
Red flag to avoid:

Typing props or events as any, or using the broad object and Function types that accept almost anything.

They may ask next:
  • How do you type a useState that starts as null and later holds a user?
  • How would you make two props mutually exclusive, so a caller can pass one or the other but not both?
Say it in 60 seconds
Hard Coding round Mid-level, Senior Practice question

23. Write a generic React list component where renderItem knows the exact type of each item.

What the interviewer is really testing:
Whether you can carry a type parameter through a component so callers get full checking without writing any types themselves.
Answer frame:

Type parameter: both the props type and the component take T.

Inference: T is inferred from the items prop, so renderItem's parameter is typed for free.

TSX quirk: a generic arrow function in a .tsx file needs <T,> so it isn't read as a JSX tag.

Sample spoken answer:

"I make the props generic, ListProps of T, with items as an array of T, renderItem as a function from T to a React node, and getKey as a function from T to a string for React's keys. Then the component itself is a generic function taking ListProps of T. The nice part is on the calling side. When someone renders List with an array of users, TypeScript infers T as User from the items prop, so inside renderItem the parameter is already a User, with autocomplete and an error if they reach for a field that doesn't exist. Nobody writes a type argument by hand. One gotcha: as an arrow function in a .tsx file, angle bracket T looks like a JSX tag, so I write T followed by a comma or use a function declaration. And wrapping it in memo can lose the type parameter, so that needs a little extra typing."

Code:
import * as React from 'react';

type ListProps<T> = {
  items: T[];
  getKey: (item: T) => string;
  renderItem: (item: T) => React.ReactNode;
};

function List<T>({ items, getKey, renderItem }: ListProps<T>) {
  return (
    <ul>
      {items.map((item) => (
        <li key={getKey(item)}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}

type User = { id: string; name: string };
declare const users: User[];

<List items={users} getKey={(u) => u.id} renderItem={(u) => <b>{u.name}</b>} />;
Red flag to avoid:

Typing items as any[], so renderItem gets any and callers lose all checking.

They may ask next:
  • How would you add a constraint so every item must have an id, and drop getKey?
  • Why can wrapping this component in memo lose the type parameter?
Say it in 60 seconds
Medium Technical round Mid-level, Senior Practice question

24. How do you type data that comes back from an API? Why isn't writing 'as User' after the fetch enough?

What the interviewer is really testing:
Whether you know types stop at the network boundary, and have a plan to validate or generate them there.
Answer frame:

The gap: response.json() gives any; an annotation or as is a promise, not a check.

Validate: treat the body as unknown and check it with a guard or a schema library, deriving the type from the schema.

Generate: where there's an API spec, generate types so both sides share one source.

Errors: return success or failure as a discriminated union.

Sample spoken answer:

"The body from fetch comes back as any. If I write as User, I've told the compiler something it can't check, and when the backend renames a field, my code compiles fine and breaks in production with undefined everywhere. So at the boundary I treat the response as unknown and validate it. For small cases, a type guard. For anything bigger, a schema library, where I describe the shape once, parse the response against it and get the TypeScript type from the schema, so the check and the type can't drift. If the backend publishes an OpenAPI spec, I'd rather generate types from it in the build. I also wrap calls so they return a discriminated union, either ok with data or not ok with an error, so every caller has to handle failure instead of assuming the happy path."

Code:
type ApiResult<T> = { ok: true; data: T } | { ok: false; error: string };

async function getJson<T>(
  url: string,
  isT: (x: unknown) => x is T,
): Promise<ApiResult<T>> {
  const res = await fetch(url);
  if (!res.ok) return { ok: false, error: `HTTP ${res.status}` };
  const body: unknown = await res.json();
  return isT(body)
    ? { ok: true, data: body }
    : { ok: false, error: 'Unexpected response shape' };
}
Red flag to avoid:

Saying the response is guaranteed to match because the type is declared, or casting every response with as.

They may ask next:
  • Where in the app would you put this validation so the rest of the code can trust its types?
  • How would you share types between a TypeScript backend and front end?
Say it in 60 seconds

Real Work 6 questions

Hard Behavioral round Mid-level, Senior Practice question

25. Tell me about moving a JavaScript codebase to TypeScript. How did you stage it so the team kept shipping?

What the interviewer is really testing:
Whether you can run a gradual migration with a plan and bring the team along, rather than attempting a big-bang rewrite.
Answer frame:

Situation: the codebase, and why the move was worth the effort.

Staging: allowJs, convert from the leaves up, tighten flags step by step.

Guardrails: new files in TypeScript, a lint rule on new any, API boundaries typed first.

Result: what got better, and what you'd do differently.

Sample spoken answer:

"At my last company we had a mid-sized JavaScript front end, and most production bugs were undefined errors from API data. A rewrite was out of the question, so I proposed a gradual move. We turned on allowJs so TypeScript and JavaScript could live side by side, started with loose settings, and set one rule: new files are TypeScript. Then we converted from the leaves up, utilities and the API client first, because typed API responses paid off everywhere else. Each sprint we took a few folders. Once most files were converted, we turned on strictNullChecks and fixed it area by area, and later full strict. I added a lint rule that flagged new explicit any, so that count only went down. Over a few months the null crashes in our error tracker dropped sharply. I'd generate the API types from a spec from day one next time."

Red flag to avoid:

Describing a stop-the-world rewrite, or a migration declared finished with any sprinkled everywhere.

They may ask next:
  • How did you handle files that were too tangled to type well at first?
  • How did you win over teammates who didn't want TypeScript?
Say it in 60 seconds
Medium Behavioral round Mid-level, Senior Practice question

26. Tell me about a production bug where the TypeScript types said one thing and the runtime did another. How did you fix it?

What the interviewer is really testing:
Whether you understand where type safety leaks in practice, and fix the root cause instead of adding another cast.
Answer frame:

Bug: what users saw, and why the compiler didn't catch it.

Trace: how you found the cast, the any or the boundary where the types went wrong.

Fix: the code fix, plus a guard so the same leak can't come back.

Sample spoken answer:

"In one project, a billing dashboard started showing strange totals for some customers, numbers that looked stuck together, while TypeScript was completely happy. I traced it to the API client. The response was cast with as Invoice, and the backend had started sending the amount as a string for some older records. Our type said number, so nothing flagged it, and adding a string to a number just joined them as text. The quick fix was converting the amount. The real fix was stopping the lie at the boundary: we replaced the cast with runtime validation on that endpoint, so a bad shape now logs a clear error instead of flowing through the app. Then I searched for other as casts on fetched data and listed them to fix. My lesson: the compiler is only as honest as the edges where data comes in."

Red flag to avoid:

Blaming TypeScript for the bug, or fixing it with yet another cast.

They may ask next:
  • How would you find every place where types can lie in a large codebase?
  • Would you validate every response at runtime, or only some? How do you decide?
Say it in 60 seconds
Hard Behavioral round Senior Practice question

27. Tell me about a time your types got too clever, hard to read or slow to compile. What did you change?

What the interviewer is really testing:
Whether you balance type safety against readability and compile time, a key judgement for a senior TypeScript engineer.
Answer frame:

Situation: what the complex type was for and who used it.

Signal: unreadable errors, a slow editor, teammates avoiding it.

Change: simpler types, explicit annotations, or moving a check to runtime or tests.

Result: what improved and the rule you took away.

Sample spoken answer:

"I once built a typed form helper for our team with deep conditional and mapped types, so every field path like address.city was checked and inferred. It worked and I was proud of it, but a few weeks later teammates were complaining. Error messages ran to pages that nobody could read, and the editor got sluggish in big forms. I ran the compiler with its extended diagnostics and saw type checking time had jumped on those files. So I simplified. I capped how deep the path types went, added explicit return types on the public functions so the compiler didn't recompute them everywhere, and swapped one clever inferred type for a plain interface people could read. We lost a little precision and gained a lot of trust. My rule now: a type is for the people using it, and if its error can't be understood, it isn't finished."

Red flag to avoid:

Showing off type gymnastics with no thought for the people who have to read the error messages.

They may ask next:
  • How do you find out where compile time is going in a TypeScript project?
  • Where do you draw the line between type-level checks and runtime tests?
Say it in 60 seconds
Medium Situational round Mid-level, Senior Practice question

28. Before a deadline, a teammate's PR adds several ts-ignore comments and casts to any to get the build green. What do you do?

What the interviewer is really testing:
Whether you can protect type safety pragmatically, without blocking the release or waving through hidden bugs.
Answer frame:

Look first: read what each suppressed error says; some are real bugs.

Split: fix real bugs now; allow justified escapes as ts-expect-error with a reason.

Follow up: a ticket for the rest, and a lint rule so every escape needs a comment.

Sample spoken answer:

"I'd start by reading what the suppressed errors actually say, because ts-ignore hides the message, not the problem. Often one or two are real bugs, a value that can be undefined or a wrong argument, and those I'd want fixed before merging, deadline or not. For the ones that are just awkward typing, like a library with poor types, I'd suggest swapping ts-ignore for ts-expect-error with a short reason. The difference matters: ts-expect-error fails the build once the error goes away, so it can't linger forever, while ts-ignore also silences any new error on that line. Then I'd agree a follow-up ticket to remove the casts, and propose a lint rule that requires a description on these comments. I'd have the conversation on a call, framed as protecting the release, not as a lecture."

Red flag to avoid:

Approving it silently to hit the date, or blocking the release over every suppression without checking what they hide.

They may ask next:
  • Why is ts-expect-error safer than ts-ignore?
  • When is casting to any genuinely acceptable?
Say it in 60 seconds
Hard Situational round Senior Practice question

29. You switch on strict in a large codebase and get a few thousand errors. The team can't pause feature work. How do you roll it out?

What the interviewer is really testing:
Whether you can plan a gradual tightening that keeps making progress without freezing delivery.
Answer frame:

Measure: count errors per flag and per folder to see where the pain is.

Stage: one flag at a time, usually strictNullChecks first, on the code that matters most.

Ratchet: CI fails on new errors while the old count only goes down, with a target date.

Sample spoken answer:

"I wouldn't flip it on for everyone at once. First I'd measure: turn on each strict flag separately and count errors per flag and per folder. Usually a couple of flags and a few folders account for most of it. Then I'd go one flag at a time, starting with the one that catches the most real bugs, usually strictNullChecks. Because even one flag can mean thousands of errors, I'd use a ratchet: record the current errors as a baseline and make CI fail if any file gets new ones. New code is clean from day one and the old count can only go down. I'd start with the code that matters most, like payments and API handling, and ask each team to clean one area per sprint rather than hold a big cleanup week. The goal is steady progress, with a date when the old settings go away."

Red flag to avoid:

Adding ts-ignore to thousands of lines in one go and calling the codebase strict.

They may ask next:
  • Which strict flag would you turn on first, and why?
  • How do you stop people from bulk-suppressing old errors just to hit the target?
Say it in 60 seconds
Medium Situational round Mid-level, Senior Practice question

30. A library you rely on ships type definitions that are wrong, and it's blocking your feature. What do you do?

What the interviewer is really testing:
Whether you can work around bad third-party types in one safe place and get them fixed at the source.
Answer frame:

Confirm: check the docs and the runtime behaviour, so you know it's the types that are wrong.

Contain: one typed wrapper or a local augmentation, not casts spread across the codebase.

Upstream: open an issue or pull request, and link it next to the workaround so it can be removed.

Sample spoken answer:

"First I'd make sure it's really the types and not my understanding, by checking the docs and logging what the function actually returns. If the types are wrong, I keep the workaround in one place. Usually that's a small wrapper module that calls the library and exposes correct types, with a single cast inside and a comment explaining why. If it's just a missing field on an interface, a module augmentation in a declaration file can add it cleanly. What I avoid is casting at every call site, because then nobody can tell which casts are workarounds and which are hiding bugs. Then I'd fix it at the source: open an issue or pull request on the library or its community types package, and link it in the comment so whoever reads the workaround knows when it can go."

Red flag to avoid:

Scattering as any across the codebase with no note of why, so the workaround can never be found again.

They may ask next:
  • How would you make sure the workaround gets removed once the upstream fix is released?
  • When would you consider replacing the library instead?
Say it in 60 seconds
Were you asked something else? Share it A person checks every question before it goes on the site. No name is shown.
For the call itself

The questions above are the prep. The call has ten more.

ClapAssist is an AI interview assistant for Mac and Windows. It listens to the interview on your computer and shows you what to say, in short lines you can read while you talk. Your resume and notes are never stored on our servers. It stays out of screen share on every plan; only you can see it.

Download ClapAssist with 10 free minutes
Mac and Windows · Stays out of screen share · No card