ForgeFrontend - Prepare, Practice, Crack
Secure checkout
Lifetime access
Instant PDF download
Free updates forever

Prepare · Practice · Crack

TypeScript Interview Questions and Answers

Short answer

TypeScript rounds centre on six things: any versus unknown versus never, interface versus type, generics with constraints, the utility types, discriminated unions, and how narrowing works. Above junior level the questions become about modelling, whether your types make bad states impossible.

The questionWhat it reveals
any vs unknown vs neverWhether you reach for any when the compiler pushes back
interface vs typeWhether you have an actual rule or repeat a blog post
Write a generic functionWhether you can add a constraint, not just a T
Name some utility typesWhether you derive types or duplicate them by hand
Model a loading/success/error stateReal modelling ability - the highest-signal question here
What does as const do?Whether you understand literal types and widening
Why is this cast wrong?Whether you know an assertion checks nothing at runtime
Type these React propsWhether the TypeScript is theory or daily work
What each TypeScript question is really probing.

What the TypeScript round is actually checking

Below mid-level the questions are vocabulary: name the utility types, explain generics, define a union. Above it, they turn into modelling problems, and almost always the same one, here is a component that fetches data, model its state. That question is a near-perfect filter, because the answer is either four booleans that permit impossible combinations, or a discriminated union where the impossible combinations cannot be written down.

The second thing being watched is what you do when the compiler disagrees with you. Every codebase has moments where the types fight back, and the difference between a developer who reaches for `any` or a cast and one who reaches for narrowing or a type predicate compounds across a team. Interviewers know this, which is why "when would you use any?" is asked far more often than the topic deserves.

What is the difference between any, unknown and never?

`any` switches type checking off for that value and everything derived from it, so it spreads. `unknown` is the safe counterpart: you can hold anything in it, but you cannot use it until you have narrowed it to something specific. `never` is the type with no possible values, the return type of a function that always throws, and what a union narrows to when every case has been handled.

The three, and the exhaustiveness check that makes never usefultypescript
1function handleAny(x: any) {
2  x.whatever.deeply.nested();   // compiles fine, crashes at runtime
3  return x * 2;                 // → no error reported anywhere
4}
5
6function handleUnknown(x: unknown) {
7  // x.length;                  // → Error: 'x' is of type 'unknown'
8  if (typeof x === "string") return x.length;   // narrowed, allowed
9  if (Array.isArray(x)) return x.length;
10  return 0;
11}
12
13function fail(message: string): never {
14  throw new Error(message);     // never returns, so the type is never
15}
16
17type Shape =
18  | { kind: "circle"; r: number }
19  | { kind: "square"; side: number };
20
21function area(s: Shape): number {
22  switch (s.kind) {
23    case "circle": return Math.PI * s.r ** 2;
24    case "square": return s.side ** 2;
25    default: {
26      const exhaustive: never = s;   // → if a new Shape is added, this ERRORS
27      return fail("unhandled: " + exhaustive);
28    }
29  }
30}

The exhaustiveness check is the part worth showing rather than describing. Assigning the narrowed value to a `never` variable in the default branch means that the day someone adds a third shape to the union, the compiler points at this function and tells them it is unhandled. That is TypeScript doing the job people claim it does, and it is a concrete answer to "what has TypeScript actually saved you from".

So when is any acceptable?

Migrating a large JavaScript file where the alternative is not migrating it at all, and in a few generic constraint positions where unknown is too strict. Everywhere else, unknown plus narrowing does the same job without giving up checking. The honest framing is that any is a deliberate, commented escape hatch, if it appears without a comment explaining why, it is usually someone silencing an error they did not understand.

interface or type - which should you use?

For describing the shape of an object they are almost interchangeable, and the honest answer starts by saying so. The real differences are that `interface` supports declaration merging, two declarations with the same name combine, and that only `type` can express unions, tuples, mapped types and conditional types.

Where they genuinely differtypescript
1interface User { id: string }
2interface User { email: string }        // declaration merging
3const u: User = { id: "1", email: "a@b.c" };
4// → both properties required; the declarations combined
5
6type Point = { x: number };
7// type Point = { y: number };          // → Error: Duplicate identifier
8
9// Only type can do these
10type Status = "idle" | "loading" | "done";      // union
11type Pair = [string, number];                   // tuple
12type Optional<T> = { [K in keyof T]?: T[K] };   // mapped type
13type Unwrap<T> = T extends Promise<infer U> ? U : T;   // conditional
14
15// Both extend, with different syntax
16interface Admin extends User { role: "admin" }
17type AdminAlias = User & { role: "admin" };
18
19const check: Unwrap<Promise<number>> = 42;
20// → number

The rule worth stating out loud: `interface` for object shapes that other code will extend, especially anything public, because merging lets consumers augment it, which is exactly how you add a property to Express's Request or to a library's global types. `type` for everything else, and particularly for anything that is a union. Saying "they are the same, pick one and be consistent" is a defensible answer only if you can then name declaration merging as the exception.

What are generics, and when do you actually need one?

A generic is a type parameter filled in at the call site, so one function stays precisely typed across many input types instead of falling back to `any`. The question candidates get wrong is not what a generic is, it is when a generic is pointless. A type parameter that appears in only one position is not doing anything a plain union could not do.

A useful generic, a constrained one, and one that should not existtypescript
1// Useful: the parameter links input and output
2function first<T>(arr: T[]): T | undefined {
3  return arr[0];
4}
5const n = first([1, 2, 3]);       // → number | undefined
6const s = first(["a"]);           // → string | undefined
7
8// Constrained: extends limits what T can be, and keeps the precision
9function prop<T, K extends keyof T>(obj: T, key: K): T[K] {
10  return obj[key];
11}
12const user = { id: "1", age: 30 };
13const age = prop(user, "age");    // → number, not string | number
14// prop(user, "nope");            // → Error: not assignable to 'id' | 'age'
15
16// Pointless: T appears once, so it is just an obscure way to write unknown
17function logIt<T>(value: T): void {
18  console.log(value);
19}
20// → identical to (value: unknown): void

`K extends keyof T` is the single most valuable pattern to have ready, because it appears in every real codebase, table column definitions, form field accessors, sort helpers, and because it demonstrates constraints, which is what separates "I know generics" from "I use generics". If you are asked to write one generic in an interview, write that one.

Which utility types should you know?

The point of the utility types is that related types should be derived from one source rather than written twice and drifting apart. A form's draft type, an API's update payload and a component's props are usually all transformations of one underlying entity, and expressing that relationship is what interviewers are looking for.

The ones that come up, deriving everything from one typetypescript
1type User = {
2  id: string;
3  name: string;
4  email: string;
5  lastLogin: Date | null;
6};
7
8type Draft = Partial<User>;              // every field optional
9type Public = Omit<User, "email">;       // drop fields
10type Identity = Pick<User, "id" | "name">;
11type Required_ = Required<Draft>;        // back to all-required
12type Frozen = Readonly<User>;
13type ById = Record<string, User>;        // index signature, typed
14
15type Login = NonNullable<User["lastLogin"]>;
16// → Date        the null is stripped
17
18async function loadUser(): Promise<User> { return {} as User; }
19type Loaded = Awaited<ReturnType<typeof loadUser>>;
20// → User        unwraps the function AND the promise
21
22const patch: Omit<Partial<User>, "id"> = { name: "New" };
23// → id can never be patched, and it is impossible to forget that rule
24console.log(patch.name);
25// → New

`Awaited<ReturnType<typeof fn>>` is the combination worth memorising, because it is how you type the result of an API call without declaring the response shape twice. And `Omit` is the one to use carefully: it does not error on a key that does not exist, so a renamed field silently makes the Omit a no-op. `Pick` fails loudly in the same situation, which is an argument for preferring it where either would work.

What is a discriminated union?

A union whose members all carry a literal field with different values, the discriminant, so checking that one field narrows the type to exactly one member. It is the single most useful pattern in application TypeScript, and the answer to almost every "how would you model this" question.

Four booleans versus one tagged uniontypescript
1// The version that permits impossible states
2type BadState = {
3  isLoading: boolean;
4  data: User | null;
5  error: Error | null;
6};
7const impossible: BadState = { isLoading: true, data: null, error: new Error("x") };
8// → compiles; loading AND errored at the same time, and every read needs a null check
9
10// The version where impossible states cannot be written
11type State =
12  | { status: "idle" }
13  | { status: "loading" }
14  | { status: "success"; data: User }
15  | { status: "error"; error: Error };
16
17function render(state: State) {
18  switch (state.status) {
19    case "idle":    return "Start";
20    case "loading": return "Loading...";
21    case "success": return state.data.name;   // → data is User, no null check
22    case "error":   return state.error.message;
23  }
24}
25// → { status: "success" } alone is an ERROR: data is missing
26// → state.data is unreachable in the "loading" branch, by construction

Two details make this answer complete. The discriminant must be a literal type, not `string`, `status: string` narrows nothing. And the payoff is not only compile-time safety: inside the success branch, `state.data` is `User` rather than `User | null`, so the optional chaining and non-null assertions that clutter the boolean version simply disappear. Redux actions, API responses and form state are all this pattern.

This is 1 of 200+ questions in the Complete Frontend Kit

Get every question with detailed answers, follow-ups and real code, plus coding challenges and a last-minute revision sheet. One-time payment, instant access.

⚡ Get the Complete Frontend Kit → ₹499

What does as const do?

It stops TypeScript widening a literal to its general type and makes the whole structure deeply readonly. Without it, `const s = "idle"` inside an object becomes `string`, which is what silently breaks a discriminated union assembled from a variable.

Widening, and deriving a union from datatypescript
1const a = { status: "loading" };
2// → { status: string }        too wide for a discriminated union
3
4const b = { status: "loading" } as const;
5// → { readonly status: "loading" }
6
7const KITS = ["js-kit", "react-kit", "complete-frontend-kit"] as const;
8type KitSlug = (typeof KITS)[number];
9// → "js-kit" | "react-kit" | "complete-frontend-kit"
10
11function buy(slug: KitSlug) { return slug; }
12console.log(buy("js-kit"));    // → js-kit
13// buy("java-kit");            // → Error: not assignable to KitSlug
14
15// KITS.push("x");             // → Error: push does not exist on a readonly tuple
16console.log(KITS.length);      // → 3

The `(typeof ARRAY)[number]` trick is the reason to care. It gives you one array you can iterate at runtime and a union type derived from it at compile time, so adding an entry updates both at once. Maintaining a separate list and union by hand is the drift that this pattern exists to prevent, and it is a good answer to "show me somewhere TypeScript removed real work".

How does narrowing work, and what is a type predicate?

TypeScript narrows a union automatically when it can see a check it understands: typeof, instanceof, the in operator, a truthiness test, or a comparison against a literal. When the check lives in a helper function, that inference is lost, and a type predicate is how you hand it back.

Built-in narrowing, a predicate, and an assertion functiontypescript
1function format(x: string | number | Date | null) {
2  if (x === null) return "";              // literal comparison
3  if (typeof x === "string") return x.trim();
4  if (x instanceof Date) return x.toISOString();
5  return x.toFixed(2);                    // → x is number here, by elimination
6}
7console.log(format(3.14159));             // → 3.14
8
9type Cat = { meow(): void };
10type Dog = { bark(): void };
11
12function isCat(pet: Cat | Dog): pet is Cat {   // the predicate
13  return "meow" in pet;
14}
15
16function speak(pet: Cat | Dog) {
17  if (isCat(pet)) pet.meow();             // → narrowed to Cat
18  else pet.bark();
19}
20
21// An assertion function narrows for the REST of the scope
22function assertString(v: unknown): asserts v is string {
23  if (typeof v !== "string") throw new Error("expected string");
24}
25
26function shout(v: unknown) {
27  assertString(v);
28  return v.toUpperCase();                 // → v is string from here on
29}
30console.log(shout("hi"));                 // → HI

Why does TypeScript reject an object with an extra property?

Because of the excess property check, which applies to object literals assigned directly to a typed target. TypeScript is structurally typed - a value is compatible if it has the required members, so a variable holding the same object is accepted. The literal is treated more strictly on the assumption that an unexpected key is usually a typo.

Structural typing, and the one place it gets stricttypescript
1type Props = { title: string };
2
3// const a: Props = { title: "Hi", subtitle: "extra" };
4// → Error: 'subtitle' does not exist in type 'Props'
5
6const obj = { title: "Hi", subtitle: "extra" };
7const b: Props = obj;
8// → fine! structural typing: obj HAS a title, so it is assignable
9
10function show(p: Props) { return p.title; }
11console.log(show(obj));        // → Hi
12
13// The surprise this causes in React
14type ButtonProps = { label: string };
15// <Button label="Buy" onClick={fn} />
16// → Error on onClick, because JSX props are an object LITERAL
17
18type Loose = Props & { [key: string]: unknown };
19const c: Loose = { title: "Hi", anything: 1 };
20console.log(c.title);          // → Hi

This is the question that most often produces a blank, because the behaviour looks inconsistent until you know the rule. Being able to say "structural typing accepts it, but object literals get an excess property check, and assigning through a variable bypasses that" is a distinctive answer, most candidates have hit the error and never learnt why.

How do you type React props, state and refs?

In practice a frontend TypeScript round is a React TypeScript round, so expect to be asked to type a component. The current conventions matter here: `React.FC` has fallen out of favour, and typing the props object directly is what modern codebases and the React team's own docs do.

The patterns a React codebase actually usestypescript
1import { useState, useRef, type ReactNode, type ComponentProps } from "react";
2
3type CardProps = {
4  title: string;
5  children: ReactNode;              // anything renderable
6  onSelect?: (id: string) => void;  // optional callback
7};
8
9function Card({ title, children, onSelect }: CardProps) {
10  const [open, setOpen] = useState(false);          // → boolean, inferred
11  const [user, setUser] = useState<User | null>(null);  // annotate when it starts null
12  const inputRef = useRef<HTMLInputElement>(null);      // → RefObject<HTMLInputElement>
13
14  console.log(inputRef.current?.value);
15  // → undefined      current is null until the element mounts
16  return null;
17}
18
19// Extend a native element instead of re-listing its props
20type ButtonProps = ComponentProps<"button"> & { variant: "primary" | "ghost" };
21// → every real button attribute, plus variant, correctly typed
22
23// Reducer state is the discriminated union again
24type Action =
25  | { type: "open" }
26  | { type: "select"; id: string };

The two details interviewers listen for are `useState<T | null>(null)`, because inference alone would fix the type as `null` forever, and `ComponentProps<"button">`, which is how you build a design-system component that accepts every native attribute without listing forty of them. Both are small, both show the TypeScript is daily work rather than revision.

Which tsconfig flags actually matter?

`strict` is the answer, and knowing what it turns on is the follow-up. It enables `strictNullChecks`, which is the flag that carries almost all of the value, without it `null` and `undefined` are assignable to every type and TypeScript cannot prevent the single most common runtime error in JavaScript. `noImplicitAny` stops untyped parameters silently becoming `any`.

Two more are worth naming because they are not in `strict` and teams argue about them. `noUncheckedIndexedAccess` makes `arr[0]` return `T | undefined`, which is correct and initially painful. `exactOptionalPropertyTypes` distinguishes a missing property from one explicitly set to `undefined`. Mentioning either signals that you have configured a project rather than inherited one, and the honest position, that they are right for a new codebase and expensive to switch on in an old one, is a good answer in itself.

What loses marks in a TypeScript round

The failures are consistent, and none of them are about knowing more syntax.

  • →Reaching for any or a cast the moment the compiler objects, instead of narrowing.
  • →Modelling state as several booleans and not recognising the impossible combinations.
  • →Explaining generics without ever showing a constraint.
  • →Saying interface and type are identical, with no exception to name.
  • →Treating `as User` as a check rather than an instruction to stop checking.
  • →Not knowing that types are erased, no type exists at runtime, so none can validate an API response.
  • →Duplicating a type by hand where Pick, Omit or ReturnType would derive it.

Frequently asked questions

What is the difference between any and unknown in TypeScript?
any disables type checking for that value and everything derived from it, so mistakes spread silently. unknown accepts any value but permits no operations until you narrow it with typeof, instanceof, a type predicate or a schema check. Prefer unknown at every boundary, API responses, JSON.parse, catch clauses.
What is the never type used for?
It is the type with no possible values: the return type of a function that always throws or loops forever, and what a union narrows to once every member has been handled. Assigning the narrowed value to a never variable in a switch default gives you an exhaustiveness check that errors the day someone adds a case.
Should I use interface or type in TypeScript?
For plain object shapes either works. Use interface when other code should be able to extend or augment it, declaration merging is how you add properties to a library's types. Use type for unions, tuples, mapped and conditional types, which interface cannot express. Consistency inside one codebase matters more than the choice.
What are generics in TypeScript?
Type parameters that are filled in where the function or type is used, so one implementation stays precisely typed across many input types. A generic is only pulling its weight when the parameter appears in more than one position or carries a constraint; a T that appears once is just a confusing way to write unknown.
What does K extends keyof T mean?
It constrains K to the property names of T, so a function like prop(obj, key) accepts only real keys and returns the precise type of that property rather than a union of all of them. It is the most reusable generic pattern in application code and the one to reach for if asked to write a generic on the spot.
Which TypeScript utility types should I know for interviews?
Partial, Required, Readonly, Pick, Omit, Record, ReturnType, Awaited and NonNullable cover almost every question. The point to make is that they let you derive related types from one source so they cannot drift apart, Awaited<ReturnType<typeof fetchUser>> types an API result without declaring the shape twice.
What is a discriminated union and why is it useful?
A union whose members share a literal field with different values, so checking that field narrows to exactly one member. It is how you make impossible states unrepresentable: modelling request state as idle, loading, success and error means loading-and-errored cannot be written, and data is non-null inside the success branch.
What does as const do in TypeScript?
It prevents literal widening and makes the value deeply readonly, so "idle" stays the literal type "idle" instead of becoming string. That matters for discriminated unions built from variables, and it enables (typeof ARRAY)[number], which derives a union type from an array you can also iterate at runtime.
What is a type guard or type predicate?
A function returning a boolean, declared with a pet is Cat return type, that tells the compiler what a true result implies. It restores narrowing that would otherwise be lost when the check moves into a helper. An assertion function, declared with asserts v is string, narrows for the rest of the scope instead.
Why does TypeScript complain about an extra property on an object literal?
Excess property checking applies to object literals assigned directly to a typed target, on the assumption that an unexpected key is a typo. Assigning the same object through a variable is accepted because TypeScript is structurally typed. This is also why an unexpected prop errors in JSX, props are always an object literal.
Does TypeScript check types at runtime?
No. All type information is erased during compilation, so the shipped JavaScript has no knowledge of your types and nothing validates a fetch response, a URL parameter or a value from localStorage. Use a runtime schema validator at those boundaries and let the inferred types flow inward from there.
Is TypeScript required for frontend jobs in India now?
For product companies and most funded startups, effectively yes, new React codebases are overwhelmingly TypeScript, and job descriptions list it as a requirement rather than a nice-to-have. Service companies are more mixed. Practically, being unable to type a React component closes more doors than any other single gap.

This is 1 of 200+ questions in the Complete Frontend Kit

Get every question with detailed answers, follow-ups and real code, plus coding challenges and a last-minute revision sheet. One-time payment, instant access.

⚡ Get the Complete Frontend Kit → ₹499

Full kit

Complete Frontend Kit · ₹499

Get it →