RSRobince Studio

Top 50 TypeScript interview questions and answers

The 50 questions interviewers ask most about TypeScript, with short, clear answers and code where it helps. The first 25 are the most common; the rest go deeper. Free, no sign-up.

All 50 questions

  1. What is the difference between an interface and a type alias in TypeScript?
  2. What is the difference between `any` and `unknown`?
  3. What is the `never` type and when is it used?
  4. What are generics in TypeScript and why are they useful?
  5. How do you constrain a generic type?
  6. What are union types and intersection types?
  7. What is a discriminated union?
  8. What is type narrowing and what techniques does TypeScript offer?
  9. What are type guards and how do you write a custom one?
  10. What is the difference between an enum and a union of string literals?
  11. What is a `const enum` and how does it differ from a regular enum?
  12. What is optional chaining and the nullish coalescing operator?
  13. What is the non-null assertion operator `!` and when should you avoid it?
  14. What are utility types? Explain `Partial`, `Required`, `Readonly`, `Pick` and `Omit`.
  15. What does `Record<K, T>` do and when would you use it?
  16. What are `ReturnType`, `Parameters`, `Exclude` and `Extract`?
  17. What is the purpose of `tsconfig.json` and which options matter most?
  18. What does `strict` mode enable in TypeScript?
  19. What does `strictNullChecks` do?
  20. What are the access modifiers in TypeScript classes?
  21. What is the difference between `readonly` and `const`?
  22. How do parameter properties work in TypeScript constructors?
  23. What is the difference between an abstract class and an interface?
  24. What is a type assertion and how does it differ from type casting?
  25. What are declaration files (`.d.ts`) and how do you use them?
  26. What are mapped types and how do you write one?
  27. What are conditional types and what does the `infer` keyword do?
  28. Why does a conditional type distribute over a union, and how do you prevent it?
  29. What are template literal types and where are they useful?
  30. What does the `keyof` operator do and how does it differ from `typeof`?
  31. What does `as const` do and why is it useful?
  32. What is the `satisfies` operator and when would you use it instead of a type annotation?
  33. What is the difference between `unknown[]` and `any[]`, and why do `catch` clauses use `unknown`?
  34. What is wrong with this code: `Object.keys(obj).forEach(k => obj[k])`?
  35. What is the difference between function overloads and a union-typed parameter?
  36. What is variance, and why is `strictFunctionTypes` important?
  37. What is structural typing and how does it differ from nominal typing?
  38. Why does excess property checking not trigger when I assign a variable instead of an object literal?
  39. How do you type a function that accepts a callback with generics so the types flow through?
  40. What are the `Awaited` and `Promise` types, and how do you type an async function's return value?
  41. How would you type a function that returns different shapes depending on an argument without overloads?
  42. What is the difference between `interface extends` and `&` intersection when combining types?
  43. How do ES modules and `import type` work in TypeScript, and why use `import type`?
  44. How do you safely handle data from an API when TypeScript types are erased at runtime?
  45. What is declaration merging and what is module augmentation?
  46. What does the `this` type and the `this` parameter do in TypeScript?
  47. How can you improve TypeScript compile performance in a large project?
  48. What is the difference between `noImplicitAny` and `noUncheckedIndexedAccess`, and when would you enable the latter?
  49. What are the common pitfalls of using `enum` and what would you use instead?
  50. How would you migrate a large JavaScript codebase to TypeScript incrementally?

1. What is the difference between an interface and a type alias in TypeScript?

Both describe the shape of data, but interfaces are open to extension while type aliases are more flexible in what they can name. Interfaces support declaration merging and extends, and are usually preferred for object shapes and class contracts. Type aliases can also name unions, intersections, tuples, primitives and mapped or conditional types, but cannot be re-opened once declared.

interface User { name: string }
interface User { age: number }      // merges into one User
type ID = string | number;          // only a type alias can do this

2. What is the difference between `any` and `unknown`?

unknown is the type-safe counterpart of any: you can assign anything to it, but you must narrow it before using it. any switches off type checking entirely, so property access and calls compile without checks and errors surface only at runtime. Prefer unknown for values of uncertain shape, such as parsed JSON or caught errors.

let a: any = "hi";
a.toFixed();                 // compiles, crashes at runtime
let u: unknown = "hi";
if (typeof u === "string") u.toUpperCase(); // must narrow first

3. What is the `never` type and when is it used?

never represents values that never occur, such as the return type of a function that always throws or loops forever. It is also the type left over when a union has been fully narrowed, which makes it useful for exhaustiveness checks in switch statements. never is assignable to every type, but no type except never is assignable to it.

type Shape = "circle" | "square";
function area(s: Shape) {
  switch (s) {
    case "circle": return 1;
    case "square": return 2;
    default: { const _x: never = s; return _x; }
  }
}

4. What are generics in TypeScript and why are they useful?

Generics let you write reusable code that works with many types while keeping type information, instead of falling back to any. You declare a type parameter such as T in angle brackets and use it in parameters and return types, and TypeScript infers it at the call site. They power typed collections, API wrappers and utility types.

function first<T>(items: T[]): T | undefined {
  return items[0];
}
const n = first([1, 2, 3]);   // number | undefined
const s = first(["a", "b"]);  // string | undefined

5. How do you constrain a generic type?

You constrain a generic with the extends keyword, which restricts the type argument to types that satisfy a given shape. This lets you safely access properties of the parameter inside the function. A common pattern is K extends keyof T to ensure a key really exists on an object.

function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}
getProp({ id: 1, name: "Ann" }, "name"); // string
// getProp({ id: 1 }, "x");               // error

6. What are union types and intersection types?

A union type A | B means a value can be one of several types, while an intersection type A & B means a value must satisfy all of them at once. Unions are used for alternatives like string | number, and you must narrow them before using type-specific members. Intersections are typically used to combine object types into one with all properties.

type Id = string | number;
type Named = { name: string };
type Aged = { age: number };
type Person = Named & Aged; // needs both name and age

7. What is a discriminated union?

A discriminated union is a union of object types that share a common literal-typed property, called the discriminant, which TypeScript uses to narrow the type. Checking that property in an if or switch tells the compiler exactly which member you have. It is the standard way to model states such as loading, success and error.

type Result =
  | { status: "ok"; data: string }
  | { status: "error"; message: string };

function show(r: Result) {
  if (r.status === "ok") return r.data;
  return r.message;
}

8. What is type narrowing and what techniques does TypeScript offer?

Type narrowing is how TypeScript refines a broad type to a more specific one inside a code branch based on runtime checks. Common techniques include typeof, instanceof, the in operator, equality and truthiness checks, discriminant properties, and user-defined type guards. The compiler tracks control flow, so the narrowed type applies only where the check is known to hold.

function len(x: string | string[]) {
  if (typeof x === "string") return x.length;
  return x.length; // x is string[] here
}

9. What are type guards and how do you write a custom one?

A type guard is an expression or function that tells the compiler a value has a narrower type within a branch. Built-in guards include typeof and instanceof, and you write custom ones with a type predicate return type of the form value is Type. TypeScript trusts your predicate, so its logic must be correct.

interface Cat { meow(): void }
interface Dog { bark(): void }
function isCat(p: Cat | Dog): p is Cat {
  return "meow" in p;
}

10. What is the difference between an enum and a union of string literals?

Enums are real runtime objects that group named constants, while a union of string literals exists only at compile time and generates no JavaScript. Literal unions are lighter, tree-shake well and are often preferred in modern code, but enums give you a named object and, for numeric enums, reverse mapping. Plain enum also emits code, so many teams avoid it or use as const objects instead.

enum Dir { Up = "UP", Down = "DOWN" }       // emits an object
type Direction = "UP" | "DOWN";             // erased at compile time

11. What is a `const enum` and how does it differ from a regular enum?

A const enum is inlined at compile time, so no enum object is emitted and each usage is replaced by its literal value. This reduces output size, but you cannot iterate over it or look up members dynamically. It also causes problems with isolatedModules and transpilers like Babel or esbuild, so many projects avoid it.

const enum Level { Low = 1, High = 2 }
const x = Level.High; // compiled output: const x = 2;

12. What is optional chaining and the nullish coalescing operator?

Optional chaining ?. safely accesses a property, element or call and returns undefined if the left side is null or undefined, instead of throwing. Nullish coalescing ?? returns the right-hand value only when the left is null or undefined, unlike ||, which also replaces 0, "" and false. They are often combined to read deep values with defaults.

const city = user?.address?.city ?? "Unknown";
const count = input.count ?? 10; // keeps 0 as a valid value

13. What is the non-null assertion operator `!` and when should you avoid it?

The postfix ! tells the compiler that a value is not null or undefined, removing those types without any runtime check. It only silences the compiler, so if you are wrong you get a runtime error. Prefer narrowing with an if check, optional chaining or a default value, and reserve ! for cases where you know something the compiler cannot.

const el = document.getElementById("app")!; // HTMLElement, not null
// safer:
const el2 = document.getElementById("app");
if (el2) el2.classList.add("ready");

14. What are utility types? Explain `Partial`, `Required`, `Readonly`, `Pick` and `Omit`.

Utility types are built-in generic types that transform existing types. Partial<T> makes all properties optional, Required<T> makes them all required, Readonly<T> makes them read-only, Pick<T, K> keeps only the chosen keys, and Omit<T, K> removes the chosen keys. They avoid duplicating type definitions.

interface User { id: number; name: string; email: string }
type Patch = Partial<User>;
type Preview = Pick<User, "id" | "name">;
type NoEmail = Omit<User, "email">;

15. What does `Record<K, T>` do and when would you use it?

Record<K, T> builds an object type whose keys are of type K and whose values are of type T. It is ideal for lookup tables and dictionaries, and when K is a union of literals the compiler forces you to provide every key. It is a cleaner alternative to an index signature when the keys are known.

type Role = "admin" | "user" | "guest";
const perms: Record<Role, string[]> = {
  admin: ["read", "write"],
  user: ["read"],
  guest: [],
};

16. What are `ReturnType`, `Parameters`, `Exclude` and `Extract`?

ReturnType<F> gets a function's return type and Parameters<F> gets its parameter types as a tuple. Exclude<T, U> removes from a union the members assignable to U, while Extract<T, U> keeps only those members. All are built on conditional types and are handy for deriving types instead of repeating them.

function make() { return { id: 1, ok: true }; }
type Made = ReturnType<typeof make>;           // { id: number; ok: boolean }
type A = Exclude<"a" | "b" | "c", "a">;        // "b" | "c"
type B = Extract<"a" | "b" | "c", "a" | "z">;  // "a"

17. What is the purpose of `tsconfig.json` and which options matter most?

tsconfig.json marks the root of a TypeScript project and tells the compiler which files to include and how to compile them. Key options include target, module, moduleResolution, strict, outDir, rootDir, lib, esModuleInterop, skipLibCheck, declaration and paths. Fields like include, exclude and extends control which files are compiled and let you share base configs.

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "strict": true,
    "outDir": "dist"
  },
  "include": ["src"]
}

18. What does `strict` mode enable in TypeScript?

Setting "strict": true turns on a family of stricter type-checking flags, including strictNullChecks, noImplicitAny, strictFunctionTypes, strictBindCallApply, strictPropertyInitialization, noImplicitThis, alwaysStrict and useUnknownInCatchVariables. It catches many bugs such as unchecked nulls and implicit any. New projects should enable it, and existing ones can adopt the flags one by one.

19. What does `strictNullChecks` do?

With strictNullChecks on, null and undefined are separate types and are not assignable to other types unless you include them explicitly in a union. This forces you to handle missing values before using them, eliminating a common source of "cannot read property of undefined" errors. With it off, every type silently accepts null and undefined.

let name: string = null;          // error with strictNullChecks
let maybe: string | null = null;  // OK
maybe.length;                     // error until narrowed

20. What are the access modifiers in TypeScript classes?

TypeScript has public, private and protected. public is the default and allows access anywhere, private limits access to the declaring class, and protected allows the class and its subclasses. These are enforced only at compile time, whereas JavaScript's #name fields are truly private at runtime.

class Account {
  public id = 1;
  private balance = 0;
  protected owner = "me";
  #secret = "x"; // runtime-private
}

21. What is the difference between `readonly` and `const`?

const applies to a variable binding and prevents reassigning it, while readonly applies to a property, index signature or array and prevents writing to it after initialization. A const object can still have its properties changed, and a readonly property can still be reassigned if the object type is cast. Both are compile-time protections, except that const is also enforced by JavaScript at runtime.

const user = { name: "Ann" };
user.name = "Bob";              // allowed
interface Pt { readonly x: number }
const p: Pt = { x: 1 };
// p.x = 2;                     // error

22. How do parameter properties work in TypeScript constructors?

Parameter properties let you declare and initialize class members directly in the constructor signature by adding an access modifier or readonly to a parameter. TypeScript generates the property and assigns the argument automatically, removing boilerplate. Note they are not allowed with erasableSyntaxOnly, which some modern setups enable.

class Point {
  constructor(public x: number, private readonly y: number) {}
}
const p = new Point(1, 2);
console.log(p.x); // 1

23. What is the difference between an abstract class and an interface?

An interface only describes a shape and is erased at compile time, while an abstract class can hold implementation, state and constructors and exists at runtime. A class can implement many interfaces but extend only one class. Use an abstract class when subclasses share behavior, and an interface when you only need a contract.

abstract class Shape {
  abstract area(): number;
  describe() { return `Area: ${this.area()}`; }
}
class Sq extends Shape {
  constructor(private s: number) { super(); }
  area() { return this.s ** 2; }
}

24. What is a type assertion and how does it differ from type casting?

A type assertion, written value as Type, tells the compiler to treat a value as a given type, but unlike casting in other languages it does no runtime conversion or checking. It is erased at compile time, so a wrong assertion can cause runtime errors. TypeScript only allows assertions between sufficiently related types, and as unknown as T bypasses this and should be a last resort.

const input = document.querySelector("#name") as HTMLInputElement;
input.value = "Ann";

25. What are declaration files (`.d.ts`) and how do you use them?

Declaration files contain only type information, with no implementation, and describe the shape of JavaScript code so TypeScript can type-check it. Many libraries ship their own, and others are available as @types/* packages from DefinitelyTyped. You can also write your own using declare to type a module or global, and enable generation for your library with the declaration compiler option.

// globals.d.ts
declare module "legacy-lib" {
  export function run(input: string): number;
}
declare const APP_VERSION: string;

26. What are mapped types and how do you write one?

A mapped type builds a new type by iterating over the keys of another type with [K in keyof T], optionally changing each property's type or modifiers. You can add or remove readonly and ? with + and - prefixes, and remap keys with as. Built-in utilities such as Partial and Readonly are mapped types.

type Mutable<T> = { -readonly [K in keyof T]: T[K] };
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type G = Getters<{ name: string }>; // { getName: () => string }

27. What are conditional types and what does the `infer` keyword do?

A conditional type has the form T extends U ? X : Y and picks a type depending on whether T is assignable to U. Inside the extends clause, infer declares a type variable that TypeScript fills in by pattern matching, letting you extract parts of a type. ReturnType and Awaited are built this way.

type ElementOf<T> = T extends (infer E)[] ? E : never;
type A = ElementOf<string[]>; // string
type B = ElementOf<number>;   // never

28. Why does a conditional type distribute over a union, and how do you prevent it?

When the checked type is a naked type parameter, a conditional type is applied to each union member separately and the results are unioned. This is called distribution and is why Exclude works. To stop it, wrap both sides in a tuple, as in [T] extends [U], which checks the union as a whole.

type ToArray<T> = T extends any ? T[] : never;
type X = ToArray<string | number>;   // string[] | number[]
type Y<T> = [T] extends [any] ? T[] : never;
type Z = Y<string | number>;         // (string | number)[]

29. What are template literal types and where are they useful?

Template literal types build string literal types using the same backtick syntax as JavaScript templates, and they distribute over unions. They are useful for typing event names, CSS class patterns, route strings and generated keys. Combined with Uppercase, Capitalize and similar helpers, they let you derive precise string types from existing ones.

type Side = "top" | "bottom";
type Prop = `margin-${Side}`;      // "margin-top" | "margin-bottom"
type Handler = `on${Capitalize<"click" | "focus">}`;

30. What does the `keyof` operator do and how does it differ from `typeof`?

keyof T produces a union of the property names of type T, while the type-level typeof x produces the type of a value. They are often combined as keyof typeof obj to get the keys of a runtime object. Note that keyof on a type with a string index signature gives string | number.

const colors = { red: "#f00", blue: "#00f" };
type ColorName = keyof typeof colors; // "red" | "blue"

31. What does `as const` do and why is it useful?

as const is a const assertion that makes TypeScript infer the narrowest literal types and marks object properties and tuples as readonly. Without it, "GET" widens to string and arrays become string[]. It is handy for deriving union types from a runtime array or object, avoiding duplicated definitions.

const METHODS = ["GET", "POST"] as const;
type Method = (typeof METHODS)[number]; // "GET" | "POST"

32. What is the `satisfies` operator and when would you use it instead of a type annotation?

satisfies checks that an expression matches a type without changing the inferred type of that expression. A plain annotation widens the value to the annotated type and loses specific information, whereas satisfies validates the shape and keeps literal keys and values. It is ideal for config objects and lookup tables.

type Palette = Record<string, string | number[]>;
const p = {
  red: [255, 0, 0],
  green: "#0f0",
} satisfies Palette;
p.red.map(n => n * 2); // still known to be number[]

33. What is the difference between `unknown[]` and `any[]`, and why do `catch` clauses use `unknown`?

Both hold anything, but any[] lets you use elements freely while unknown[] forces narrowing first. Anything can be thrown in JavaScript, not just Error objects, so with useUnknownInCatchVariables (part of strict) the caught value is unknown. You must check it, for example with instanceof Error, before reading .message.

try {
  risky();
} catch (e) {
  if (e instanceof Error) console.log(e.message);
  else console.log(String(e));
}

34. What is wrong with this code: `Object.keys(obj).forEach(k => obj[k])`?

It fails under strict because Object.keys returns string[], not (keyof T)[], so indexing the object with a plain string is an implicit any error. TypeScript does this on purpose, since objects may have extra properties at runtime. Fix it by casting the key to keyof typeof obj when you know the shape, or by iterating Object.entries.

const obj = { a: 1, b: 2 };
(Object.keys(obj) as (keyof typeof obj)[]).forEach(k => {
  console.log(obj[k]); // number
});

35. What is the difference between function overloads and a union-typed parameter?

Overloads declare several call signatures so the return type can depend on the argument type, while a union parameter gives one signature with one return type. Use overloads when output varies with input; otherwise a union or a generic is simpler. The implementation signature is not visible to callers and must be compatible with every overload.

function toArr(x: string): string[];
function toArr(x: number): number[];
function toArr(x: string | number) {
  return [x];
}
const a = toArr("hi"); // string[]

36. What is variance, and why is `strictFunctionTypes` important?

Variance describes how subtyping of a type affects subtyping of types built from it. Function parameters are contravariant: a function accepting a wider type can stand in for one accepting a narrower type, not the reverse. strictFunctionTypes enforces this for function-typed properties, but method syntax stays bivariant for compatibility, which is why Array methods remain loosely checked.

type Handler = (e: MouseEvent) => void;
const onAny: (e: Event) => void = () => {};
const ok: Handler = onAny; // fine, wider param
// const bad: (e: Event) => void = (e: MouseEvent) => {}; // error

37. What is structural typing and how does it differ from nominal typing?

In structural typing, compatibility depends on the shape of a type rather than its declared name, so two unrelated types with the same members are interchangeable. This is why object literals can satisfy interfaces without declaring them. When you need nominal-like safety, for example separate UserId and OrderId, you use branded types.

type UserId = string & { readonly __brand: "UserId" };
type OrderId = string & { readonly __brand: "OrderId" };
declare const u: UserId;
// const o: OrderId = u; // error

38. Why does excess property checking not trigger when I assign a variable instead of an object literal?

Excess property checks only apply to fresh object literals assigned directly to a typed target. When you pass an existing variable, TypeScript uses normal structural assignability, which allows extra properties. This is a common source of confusion, and the check can be bypassed accidentally by first storing the object in a variable.

interface Opts { size: number }
const o = { size: 1, color: "red" };
const a: Opts = o;                       // OK
// const b: Opts = { size: 1, color: "red" }; // error

39. How do you type a function that accepts a callback with generics so the types flow through?

Declare a type parameter on the outer function and use it in both the callback and the return type, and TypeScript infers it from the arguments. Avoid separate unrelated parameters, which would force any. Constraining with extends keeps the callback type safe when you need specific members.

function mapArr<T, U>(items: T[], fn: (item: T) => U): U[] {
  return items.map(fn);
}
const lens = mapArr(["a", "bb"], s => s.length); // number[]

40. What are the `Awaited` and `Promise` types, and how do you type an async function's return value?

An async function always returns a Promise, so a function returning string is typed Promise<string>. Awaited<T> unwraps promise types recursively and gives the value you get after await, which is useful with Promise.all and with ReturnType of async functions. You normally let TypeScript infer the return type but annotate it for public APIs.

async function load() { return { id: 1 }; }
type Data = Awaited<ReturnType<typeof load>>; // { id: number }

41. How would you type a function that returns different shapes depending on an argument without overloads?

Use a generic that extends a key of a lookup type and index into it for the return type. This ties the output to the literal argument passed in, and it scales better than writing one overload per case. The mapping type becomes the single source of truth.

interface Events { click: MouseEvent; key: KeyboardEvent }
function makeEvent<K extends keyof Events>(name: K): Events[K] {
  return {} as Events[K];
}
const e = makeEvent("click"); // MouseEvent

42. What is the difference between `interface extends` and `&` intersection when combining types?

Both combine members, but extends reports an error immediately if the properties conflict, while an intersection silently produces never for conflicting properties. Interfaces with extends are also cached by name and are generally faster for the compiler on large codebases. Prefer extends for object shapes and intersections for composing unions or ad-hoc types.

interface A { x: string }
// interface B extends A { x: number } // error
type C = A & { x: number };            // x becomes never

43. How do ES modules and `import type` work in TypeScript, and why use `import type`?

import type imports a symbol for type positions only and is always erased from the emitted JavaScript. It avoids accidental runtime imports, helps break circular dependencies and is required for re-exporting types under isolatedModules. With verbatimModuleSyntax, imports without type are kept exactly as written.

import type { User } from "./models";
import { type Role, load } from "./api";
export type { User };

44. How do you safely handle data from an API when TypeScript types are erased at runtime?

Treat the response as unknown and validate it at runtime, because annotations such as as User are not checked. You can write type guards by hand or use a schema library like Zod, which validates data and infers the static type from the same schema. This keeps the compile-time type and the runtime reality in sync.

import { z } from "zod";
const User = z.object({ id: z.number(), name: z.string() });
type User = z.infer<typeof User>;
const user = User.parse(await res.json()); // throws if invalid

45. What is declaration merging and what is module augmentation?

Declaration merging is when the compiler combines multiple declarations with the same name, such as two interfaces or a namespace and a function, into one definition. Module augmentation uses it to extend types from another module or global scope, for example adding a field to Express's Request. The file must be a module, so it needs at least one top-level import or export.

// express.d.ts
import "express";
declare module "express-serve-static-core" {
  interface Request { userId?: string }
}

46. What does the `this` type and the `this` parameter do in TypeScript?

A fake first parameter named this declares what this must be inside a function, and TypeScript erases it from the emitted code. It catches errors when a method is detached from its object. Within classes, the polymorphic this return type enables fluent, chainable methods that stay correct in subclasses.

function greet(this: { name: string }) {
  return "Hi " + this.name;
}
// greet();  // error: this is void
greet.call({ name: "Ann" });

47. How can you improve TypeScript compile performance in a large project?

Enable incremental builds and project references so only changed packages are rechecked, and set skipLibCheck to avoid checking declaration files. Prefer interface extends over heavy intersections, avoid deeply recursive conditional types, and annotate return types of exported functions. Use tsc --extendedDiagnostics or --generateTrace to find slow spots, and let a fast transpiler handle emit while tsc only type-checks.

{
  "compilerOptions": {
    "incremental": true,
    "skipLibCheck": true,
    "noEmit": true
  }
}

48. What is the difference between `noImplicitAny` and `noUncheckedIndexedAccess`, and when would you enable the latter?

noImplicitAny errors when a type cannot be inferred and would silently become any. noUncheckedIndexedAccess is separate and not part of strict: it adds undefined to results from index signatures and array indexing, since the element may not exist. Enable it when you want to catch out-of-bounds access, accepting extra narrowing in return.

const arr: string[] = [];
const first = arr[0];     // string | undefined with the flag
first.toUpperCase();      // error until you check first

49. What are the common pitfalls of using `enum` and what would you use instead?

Numeric enums accept any number in older versions of the compiler and add reverse mappings, string enums are not interchangeable with their literal values, and regular enums emit runtime code that some transpilers and erasableSyntaxOnly disallow. A common alternative is a plain object marked as const plus a derived union type, which is erasable, tree-shakeable and works with plain strings.

const Status = { Active: "active", Off: "off" } as const;
type Status = (typeof Status)[keyof typeof Status];
const s: Status = "active"; // OK

50. How would you migrate a large JavaScript codebase to TypeScript incrementally?

Start with allowJs and checkJs off, add a basic tsconfig.json, and convert files one at a time from .js to .ts, beginning with leaf modules that have few dependencies. Turn on strict flags gradually, use // @ts-expect-error sparingly for known gaps, add @types/* packages, and type the boundaries first, such as API responses and shared models. Keep the build and tests green after every step.

{
  "compilerOptions": {
    "allowJs": true,
    "checkJs": false,
    "strict": false,
    "noEmit": true
  }
}

More Frontend interview questions

HTML · CSS · JavaScript · JavaScript in the Browser · React · Angular · All interview questions