InterviewHack.ai
Empezar gratis
Blog/TypeScript Interview Questions and How to Answer Them (45+ Questions)

TypeScript Interview Questions and How to Answer Them (45+ Questions)

September 16, 2026

typescriptjavascript

A comprehensive, code-backed reference covering 47 TypeScript interview questions across beginner, intermediate, and senior levels. Topics include type system fundamentals, structural typing, generics, conditional types, infer, mapped types, template literal types, utility types, discriminated unions, covariance/contravariance, branded types, recursive types, decorators, and real-world design patterns.

TypeScript Interview Questions and How to Answer Them (45+ Questions)

TypeScript has gone from a Microsoft experiment to the default language for serious frontend and backend work. As of 2026, well over three-quarters of professional JavaScript developers use it regularly—but hiring managers consistently report that fewer than 40% of candidates can demonstrate *advanced* type system knowledge. That gap is where interviews are won or lost.

This guide covers 47 questions across three tiers—beginner, intermediate, and senior—with real code, honest answers, and the signals interviewers are actually looking for. Work through every section, not just the "hard" ones. Fumbling a basic question after nailing a recursive type makes a bad impression.


How to Use This Guide

Each question lists:

  • The question exactly as an interviewer would ask it
  • What the interviewer is actually testing
  • A complete, code-backed answer
  • Common mistakes or gotchas to avoid

Tier 1: Fundamentals (Questions 1–15)


1. What problem does TypeScript solve, and when would you *not* use it?

What is being tested: Whether you understand the value proposition—not just the sales pitch—and whether you can think critically.

Answer:

TypeScript's core promise is moving entire classes of bugs from runtime to compile time. The most expensive bugs in production JavaScript are: calling a method on undefined, passing the wrong argument type to a function, accessing a property that was renamed, and forgetting to handle a null case. TypeScript catches all of these at compile time with zero runtime cost.

Beyond correctness, TypeScript dramatically improves the development experience. IDEs can provide accurate autocompletion, in-editor documentation, and safe rename-refactors across large codebases—none of which work reliably in JavaScript.

When not to use it: Small scripts, quick prototypes, or projects where the overhead of a build step genuinely outweighs the benefit. Configuration files, one-off automation scripts, or codebases that will never grow past a few hundred lines are reasonable candidates for plain JavaScript. The key honest answer is: the larger and longer-lived the codebase, the more TypeScript pays for itself.


2. What is type inference and how far does it go?

What is being tested: Whether you understand that TypeScript does not require explicit annotations everywhere.

Answer:

TypeScript can infer types from assignments, return statements, and contextual positions without explicit annotations:

typescript
// All types inferred—no annotations needed
const name = "Alice";           // string
const count = 42;               // number
const flags = [true, false];    // boolean[]

function double(n: number) {
  return n * 2;                 // return type inferred as number
}

const result = double(5);       // number

TypeScript also infers within generics:

typescript
function identity<T>(value: T): T {
  return value;
}

const x = identity("hello"); // T inferred as string

Limits: TypeScript cannot infer parameter types on standalone function declarations (you must annotate them), and inference weakens when assignments are separated from declarations or when values flow through any. The noImplicitAny compiler flag makes these cases errors rather than silent assumptions.


3. What is the difference between `interface` and `type`?

What is being tested: Whether you know the real behavioral differences, not just "use interface for objects."

Answer:

The most important differences:

Interfaces can be merged; type aliases cannot:

typescript
interface User { name: string; }
interface User { age: number; }
// Result: User has both name and age

type Config = { debug: boolean; };
type Config = { verbose: boolean; }; // Error: duplicate identifier

Type aliases can describe unions, tuples, and primitives; interfaces cannot:

typescript
type ID = string | number;
type Point = [number, number];
type Callback = () => void;

Both support extends, but syntax differs:

typescript
interface Admin extends User { role: string; }
type AdminUser = User & { role: string };

Practical guidance: Use interface for public API shapes that consumers might extend (libraries, design systems). Use type for unions, tuples, or cases where you explicitly do *not* want declaration merging. For internal application code, pick one and be consistent—the differences rarely matter.


4. What does `strict: true` actually enable?

What is being tested: Whether you understand what you're opting into—not just that it exists.

Answer:

strict: true in tsconfig.json is a shorthand that enables a group of flags:

| Flag | Effect |

|------|--------|

| strictNullChecks | null and undefined are not assignable to other types unless explicitly included |

| noImplicitAny | Parameters without a type annotation must be inferred as any explicitly |

| strictFunctionTypes | Function parameters are checked contravariantly (see Q36) |

| strictBindCallApply | bind, call, and apply are type-checked against the function signature |

| strictPropertyInitialization | Class properties must be initialized in the constructor |

| noImplicitThis | this in function bodies must have an explicit type |

| useUnknownInCatchVariables | catch (e) gives e the type unknown instead of any |

The single most impactful flag is strictNullChecks. Without it, null and undefined are assignable to every type, which means TypeScript silently allows the most common runtime error in JavaScript.

typescript
// strictNullChecks: false (bad)
let name: string = null; // allowed, but crashes at runtime on .toUpperCase()

// strictNullChecks: true (correct)
let name: string = null; // Error: Type 'null' is not assignable to type 'string'
let safeName: string | null = null; // OK—explicit

5. Explain the difference between `any`, `unknown`, and `never`.

What is being tested: Type-system literacy. This is asked at every level and the quality of the answer separates candidates.

Answer:

any is an escape hatch that disables the type checker entirely. Assigning to any or reading from any performs no type checks. Use it only when genuinely necessary (bridging untyped third-party code), and treat its presence in a codebase as technical debt.

unknown is the type-safe counterpart to any. A value of type unknown can hold anything, but TypeScript will not let you *use* it without first narrowing it to a specific type:

typescript
function processInput(input: unknown) {
  input.toUpperCase(); // Error: Object is of type 'unknown'

  if (typeof input === "string") {
    input.toUpperCase(); // OK—narrowed to string
  }
}

unknown is the right choice for API responses, JSON.parse results, and error boundaries.

never represents a value that can *never* exist. It appears in two contexts:

  1. 1Exhaustive checks: the bottom of a union after all cases are handled
  2. 2Functions that never return: thrown errors, infinite loops
typescript
type Shape = { kind: "circle" } | { kind: "square" };

function area(s: Shape): number {
  if (s.kind === "circle") return Math.PI;
  if (s.kind === "square") return 1;

  const _exhaustive: never = s; // Compile error if a new Shape is added
  return _exhaustive;
}

Quick mental model: any = anything, no rules; unknown = anything, prove it first; never = nothing can ever be here.


6. What are union types and intersection types? When would you use each?

Answer:

Union (|) means "one of these types." Use it when a value can legitimately be several different things:

typescript
type ID = string | number;
type Status = "pending" | "active" | "closed";

function getID(): string | number { return Math.random() > 0.5 ? "abc" : 42; }

Intersection (&) means "all of these types simultaneously." Use it to compose multiple shapes into one:

typescript
type Timestamped = { createdAt: Date; updatedAt: Date };
type Entity = { id: string; name: string };
type User = Entity & Timestamped;
// User must have id, name, createdAt, and updatedAt

Common gotcha: Intersecting primitive literals produces never:

typescript
type X = "a" & "b"; // never—can't be both "a" and "b"

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

What is being tested: One of the most conceptually important aspects of TypeScript's design.

Answer:

TypeScript uses structural typing: two types are compatible if they have the same shape (same properties and methods), regardless of their names or how they were declared.

typescript
interface Point { x: number; y: number; }

// This object was never declared as Point—but it is one
const location = { x: 10, y: 20, label: "home" };

let p: Point = location; // OK—location has everything Point requires (and more)

Nominal typing (Java, C#, Swift) requires explicit type declarations. A class must say implements Point or extend it to be considered a Point.

Practical implication: TypeScript accepts any object with *at least* the required properties. This enables powerful patterns like ad-hoc duck typing, but it also means two separately declared classes with identical shapes are mutually assignable—which can be surprising.

When nominal behavior is needed: Use branded types (Q42) to simulate nominal typing for primitives like IDs.


8. What is the difference between `null` and `undefined` in TypeScript?

Answer:

Both represent the absence of a value, but they signal different intent:

  • undefined: a variable has been declared but not assigned, or an optional property is absent, or a function parameter was not provided
  • null: an explicit, intentional "no value"—often used to clear a field or represent a missing optional foreign-key relationship

Under strictNullChecks, both must be explicitly included in a type to use them:

typescript
let a: string | undefined; // not yet assigned
let b: string | null = null; // explicitly empty

function greet(name?: string) { // name: string | undefined
  console.log(`Hello ${name ?? "stranger"}`);
}

Interviewer tip: Candidates who can explain the *semantic difference* (undefined = absent/not-yet, null = explicitly-nothing) score better than those who just say "they're both falsy."


9. What are tuples and when are they better than arrays?

Answer:

A tuple is a fixed-length array where each position has a known type:

typescript
type Coordinate = [number, number];
type NamedValue = [string, number];
type HttpResponse = [number, string, unknown]; // [statusCode, statusText, body]

Use tuples when:

  • Position carries meaning (like the return of useState)
  • The length is fixed
  • Each element has a different type
typescript
function useState<T>(initial: T): [T, (val: T) => void] {
  let state = initial;
  return [state, (val) => { state = val; }];
}

const [count, setCount] = useState(0); // count: number, setCount: (val: number) => void

Labeled tuples (TypeScript 4.0+) improve readability:

typescript
type Range = [start: number, end: number];

10. How does TypeScript handle `readonly` vs `const`?

Answer:

const applies to variable bindings—the variable cannot be reassigned. It says nothing about the value's mutability.

readonly applies to *properties* of objects and array types. It prevents property reassignment after initialization.

typescript
const user = { name: "Alice" };
user.name = "Bob"; // OK—const only prevents reassigning user itself
user = {};         // Error—cannot reassign const

interface User {
  readonly id: number;
  name: string;
}

const u: User = { id: 1, name: "Alice" };
u.id = 2;   // Error: cannot assign to 'id' because it is a read-only property
u.name = "Bob"; // OK

ReadonlyArray / readonly T[] removes all mutation methods:

typescript
const items: readonly number[] = [1, 2, 3];
items.push(4); // Error: Property 'push' does not exist on type 'readonly number[]'

11. What is type assertion and when should you use it?

Answer:

Type assertion (as T) tells TypeScript "trust me, I know the type of this value." It does not perform any runtime conversion.

typescript
const input = document.getElementById("name") as HTMLInputElement;
input.value; // OK—HTMLElement doesn't have .value, but HTMLInputElement does

When to use it: When you have information TypeScript cannot infer—such as after a DOM query, after parsing JSON from a known source, or when bridging third-party untyped code.

When to avoid it: As a workaround to pass type checks you haven't actually satisfied. The double-assertion hack (value as unknown as SomeType) is a red flag that the types are wrong somewhere.

Prefer type guards over assertions when the type can be verified at runtime:

typescript
// Bad—assert without proof
const data = JSON.parse(raw) as UserProfile;

// Better—guard with proof
function isUserProfile(x: unknown): x is UserProfile {
  return typeof x === "object" && x !== null && "id" in x && "name" in x;
}

12. What is the non-null assertion operator (`!`) and when is it a code smell?

Answer:

The postfix ! tells TypeScript that a value is definitely not null or undefined, even if the type says otherwise:

typescript
const el = document.getElementById("app")!; // HTMLElement, not HTMLElement | null
el.innerHTML = "hello";

When it's acceptable: When you can guarantee the value exists by construction—often in test setup or when TypeScript's flow analysis cannot follow a logical invariant.

When it's a smell: When used to silence an error you haven't actually resolved. Every ! is a promise you're making to the compiler. If you're wrong, you get a runtime null-pointer error with no compile-time warning. Prefer defensive checks (??, ?., or a type guard) over assertions.


13. What are index signatures and what are their limitations?

Answer:

Index signatures allow objects with dynamic keys:

typescript
interface StringMap {
  [key: string]: string;
}

const headers: StringMap = {
  "Content-Type": "application/json",
  "Authorization": "Bearer token"
};

Limitations:

  1. 1All values must match the index signature type, which forces known properties to also match:
typescript
interface BadConfig {
  [key: string]: string;
  timeout: number; // Error: 'number' is not assignable to 'string'
}
  1. 2Index signatures make every property access return T | undefined under noUncheckedIndexedAccess:
typescript
const m: StringMap = {};
const val = m["missing"]; // string | undefined under noUncheckedIndexedAccess

Alternatives: Record for simple maps; Map for runtime key-value structures where you need .has() semantics.


14. What is the difference between `extends` and `implements`?

Answer:

extends inherits behavior and structure from a parent. For classes, it copies both the type contract and the implementation:

typescript
class Animal {
  constructor(public name: string) {}
  move(distance: number) {
    console.log(`${this.name} moved ${distance}m`);
  }
}

class Dog extends Animal {
  bark() { console.log("Woof!"); }
}

implements declares that a class fulfills a contract (interface or abstract class) but receives no implementation. The class must provide every member itself:

typescript
interface Serializable {
  serialize(): string;
}

class User implements Serializable {
  constructor(public name: string) {}
  serialize() { return JSON.stringify(this); } // must implement
}

Key distinction: extends = "is a"; implements = "behaves like a." A class can implement multiple interfaces but extend only one class.


15. What is the `satisfies` operator and why was it added in TypeScript 4.9?

What is being tested: Awareness of modern TypeScript features and the nuanced problem they solve.

Answer:

satisfies validates that a value matches a type *without widening* the value's inferred type to that type. Before satisfies, you had a choice between type safety and literal-type precision:

typescript
type Palette = "red" | "green" | "blue";
type ColorMap = Record<Palette, string | [number, number, number]>;

// Option 1: annotate (safe but loses literal types)
const colors: ColorMap = {
  red: "#ff0000",
  green: [0, 255, 0],
  blue: "#0000ff",
};
colors.red.toUpperCase(); // Error: string | [number, number, number] doesn't have .toUpperCase

// Option 2: satisfies (safe AND preserves literal types)
const colors2 = {
  red: "#ff0000",
  green: [0, 255, 0],
  blue: "#0000ff",
} satisfies ColorMap;

colors2.red.toUpperCase(); // OK—TypeScript knows .red is string
colors2.green.map((v) => v * 2); // OK—TypeScript knows .green is [number, number, number]

satisfies is the right tool for: config objects, design tokens, route maps, and anywhere you want TypeScript to validate completeness while preserving precise types for downstream use.


Tier 2: Intermediate (Questions 16–32)


16. What are generics and why are they useful?

Answer:

Generics allow you to write code that works over multiple types without losing type information. The type parameter acts as a variable at the type level:

typescript
// Without generics—loses type information
function first(arr: any[]): any {
  return arr[0];
}

// With generics—type flows through
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}

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

Generics can be constrained with extends:

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

const user = { name: "Alice", age: 30 };
const name = getProperty(user, "name"); // string
const age = getProperty(user, "age");   // number
getProperty(user, "email");             // Error: not a key of user

17. Explain type narrowing and list the different techniques.

Answer:

Type narrowing is the process by which TypeScript refines a type to something more specific within a code block, based on runtime checks. TypeScript tracks these checks through control flow.

Techniques:

typeof guard:

typescript
function handle(x: string | number) {
  if (typeof x === "string") {
    x.toUpperCase(); // string
  } else {
    x.toFixed(2);   // number
  }
}

instanceof guard:

typescript
function process(err: Error | null) {
  if (err instanceof TypeError) {
    err.message; // TypeError
  }
}

in operator:

typescript
type Cat = { meow(): void };
type Dog = { bark(): void };

function speak(animal: Cat | Dog) {
  if ("meow" in animal) {
    animal.meow(); // Cat
  } else {
    animal.bark(); // Dog
  }
}

Discriminated union narrowing:

typescript
type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "rect"; width: number; height: number };

function area(s: Shape): number {
  switch (s.kind) {
    case "circle": return Math.PI * s.radius ** 2;
    case "rect": return s.width * s.height;
  }
}

User-defined type guard:

typescript
function isString(x: unknown): x is string {
  return typeof x === "string";
}

Truthiness narrowing (null/undefined checks):

typescript
function print(name: string | null) {
  if (name) {
    name.length; // string (null is falsy)
  }
}

Assertion functions (TypeScript 3.7+):

typescript
function assert(condition: unknown, msg: string): asserts condition {
  if (!condition) throw new Error(msg);
}
// After assert(x !== null, "x must exist"), TypeScript knows x is not null

18. What are mapped types?

Answer:

Mapped types create new types by iterating over the keys of an existing type and transforming each property:

typescript
type Readonly<T> = {
  readonly [K in keyof T]: T[K];
};

type Partial<T> = {
  [K in keyof T]?: T[K];
};

type Nullable<T> = {
  [K in keyof T]: T[K] | null;
};

Mapped type modifiers add or remove readonly and ?:

typescript
type Required<T> = {
  [K in keyof T]-?: T[K]; // -? removes optionality
};

type Mutable<T> = {
  -readonly [K in keyof T]: T[K]; // -readonly removes readonly
};

Key remapping (TypeScript 4.1+) with as:

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

type User = { name: string; age: number };
type UserGetters = Getters<User>;
// { getName: () => string; getAge: () => number }

19. What are conditional types?

Answer:

Conditional types choose between two possible types based on a condition expressed as a type relationship test:

typescript
type IsString<T> = T extends string ? "yes" : "no";

type A = IsString<string>; // "yes"
type B = IsString<number>; // "no"

They distribute over union types—meaning when T is a union, the conditional is applied to each member:

typescript
type NonNullable<T> = T extends null | undefined ? never : T;

type X = NonNullable<string | null | undefined>; // string

Practical example: Extract the element type of an array:

typescript
type ElementType<T> = T extends (infer E)[] ? E : never;

type A = ElementType<string[]>; // string
type B = ElementType<[number, boolean]>; // number | boolean

Gotcha: Non-distributive conditional types require wrapping in a tuple:

typescript
type IsNever<T> = [T] extends [never] ? true : false;
// Without tuple wrapping, T extends never distributes incorrectly

20. Explain the `infer` keyword.

What is being tested: One of the most powerful and tested advanced features.

Answer:

infer is used within a conditional type to capture and name a type that TypeScript should infer from the matched structure. It is only valid in the extends clause of a conditional type:

typescript
// Extract return type of a function
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type A = ReturnType<() => number>; // number
type B = ReturnType<(s: string) => boolean>; // boolean

Extract parameters:

typescript
type Parameters<T> = T extends (...args: infer P) => any ? P : never;

type C = Parameters<(a: string, b: number) => void>; // [string, number]

Extract resolved value from Promise:

typescript
type Awaited<T> = T extends Promise<infer R> ? Awaited<R> : T;

type D = Awaited<Promise<Promise<string>>>; // string (recursive unwrapping)

Extract first argument only:

typescript
type FirstArg<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;

type E = FirstArg<(x: number, y: string) => void>; // number

The mental model: infer R is like saying "whatever TypeScript figures out this position is, call it R and I'll use it."


21. What are template literal types?

Answer:

Template literal types construct new string types using the same syntax as JavaScript template literals:

typescript
type Greeting = `Hello, ${string}`;
const g: Greeting = "Hello, World"; // OK
const bad: Greeting = "Hi, World";  // Error

type EventName<T extends string> = `${T}Changed`;
type UserEvent = EventName<"name" | "email">; // "nameChanged" | "emailChanged"

Combined with mapped types for type-safe event systems:

typescript
type ChangeHandlers<T extends object> = {
  [K in keyof T as `on${Capitalize<string & K>}Changed`]: (value: T[K]) => void;
};

type UserSettings = { theme: string; language: string };
type Handlers = ChangeHandlers<UserSettings>;
// { onThemeChanged: (value: string) => void; onLanguageChanged: (value: string) => void }

Intrinsic string manipulation types (built-in):

typescript
type A = Uppercase<"hello">;   // "HELLO"
type B = Lowercase<"HELLO">;   // "hello"
type C = Capitalize<"hello">;  // "Hello"
type D = Uncapitalize<"Hello">; // "hello"

22. Walk through the main utility types and when to use each.

Answer:

Partial — makes all properties optional. Use for update payloads where not every field is required:

typescript
type UpdateUser = Partial<User>; // { name?: string; email?: string; ... }

Required — opposite of Partial. Use to enforce that all optionals are provided:

typescript
type CompleteConfig = Required<AppConfig>;

Pick — selects only the listed keys. Use for projection—showing only what's needed:

typescript
type UserPreview = Pick<User, "id" | "name">;

Omit — removes the listed keys. Use to exclude sensitive or internal fields:

typescript
type PublicUser = Omit<User, "passwordHash" | "internalId">;

Record — creates an object type with keys of type K and values of type V:

typescript
type RolePermissions = Record<"admin" | "editor" | "viewer", string[]>;

Exclude — removes union members that are assignable to U:

typescript
type NonString = Exclude<string | number | boolean, string>; // number | boolean

Extract — keeps only union members assignable to U:

typescript
type OnlyStrings = Extract<string | number | boolean, string>; // string

NonNullable — removes null and undefined:

typescript
type Name = NonNullable<string | null | undefined>; // string

ReturnType — extracts the return type of a function:

typescript
function getUser() { return { id: 1, name: "Alice" }; }
type User = ReturnType<typeof getUser>; // { id: number; name: string }

Parameters — extracts parameter types as a tuple:

typescript
type P = Parameters<typeof getProperty>; // [obj: ..., key: ...]

Awaited — unwraps a Promise type recursively:

typescript
type R = Awaited<Promise<string>>; // string

23. What are discriminated unions and why are they preferred over class hierarchies?

Answer:

A discriminated union is a union of types that each share a common literal property (the "discriminant") that allows TypeScript to narrow to the exact type:

typescript
type RemoteData<T> =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: T }
  | { status: "error"; error: Error };

function render<T>(state: RemoteData<T>) {
  switch (state.status) {
    case "idle":    return "Ready";
    case "loading": return "Loading...";
    case "success": return `Data: ${state.data}`; // T known here
    case "error":   return `Error: ${state.error.message}`;
  }
}

Why prefer over class hierarchies:

  1. 1No runtime overhead—pure type-level modeling
  2. 2Exhaustiveness checking with never catches missing cases at compile time
  3. 3Works with plain objects—no new, no instanceof
  4. 4Serializable by default (classes require special handling)
  5. 5Easier to add new states without modifying existing code

Exhaustiveness check pattern:

typescript
function assertNever(x: never): never {
  throw new Error(`Unhandled case: ${JSON.stringify(x)}`);
}

// Add to end of switch to catch missing cases
default: return assertNever(state);

24. How does declaration merging work?

Answer:

Declaration merging is TypeScript's mechanism for combining multiple declarations with the same name into a single definition. It applies to interfaces, namespaces, and certain combinations:

Interface merging:

typescript
interface Window {
  customProperty: string;
}

interface Window {
  anotherProperty: number;
}

// Effective type: { customProperty: string; anotherProperty: number; ... }

Namespace merging with classes (inner class pattern):

typescript
class Chart {
  label: Chart.LabelConfig;
}

namespace Chart {
  export interface LabelConfig {
    font: string;
    color: string;
  }
}

Namespace merging with functions (adding properties):

typescript
function validate(x: string): boolean {
  return validate.rules.every(r => r(x));
}

namespace validate {
  export const rules: Array<(s: string) => boolean> = [];
}

Module augmentation (extending third-party types):

typescript
// In your project
import { Request } from "express";

declare module "express" {
  interface Request {
    user?: { id: string; role: string };
  }
}

// Now throughout your app, req.user is typed
app.use((req, res, next) => {
  req.user = { id: "123", role: "admin" };
  next();
});

25. What is module augmentation and what are its limitations?

Answer:

Module augmentation lets you extend the types of an existing module without modifying its source. It uses declare module:

typescript
// Adding a method to Observable from a library
import { Observable } from "rxjs";

declare module "rxjs" {
  interface Observable<T> {
    debug(label: string): Observable<T>;
  }
}

Observable.prototype.debug = function(label) {
  return this.pipe(tap(v => console.log(label, v)));
};

Limitations:

  1. 1You can only add to *existing* declarations—you cannot create new top-level exports in an augmentation
  2. 2Default exports cannot be augmented—only named exports
  3. 3The augmentation file must itself be a module (must have at least one import or export)
  4. 4Global augmentation (declare global) should be used sparingly—it affects every file in the project

26. What are function overloads and when do you need them?

Answer:

Function overloads let you describe a function that behaves differently depending on the types of its arguments:

typescript
function createElement(tag: "div"): HTMLDivElement;
function createElement(tag: "span"): HTMLSpanElement;
function createElement(tag: "input"): HTMLInputElement;
function createElement(tag: string): HTMLElement {
  return document.createElement(tag);
}

const div = createElement("div");   // HTMLDivElement—not just HTMLElement
const span = createElement("span"); // HTMLSpanElement

When to use overloads:

  • Return type depends on the specific argument type
  • Completely different parameter shapes are valid
  • The generic version would be too permissive

When generics are better: When the relationship between input and output type is consistent and parametric:

typescript
// Prefer generic—same shape for any T
function wrap<T>(val: T): { value: T } {
  return { value: val };
}

Important: The implementation signature (the last one) is not part of the public API—callers can only see the overload signatures.


27. What are `keyof` and indexed access types?

Answer:

keyof T produces a union of all property names of T:

typescript
interface User { id: number; name: string; email: string; }
type UserKey = keyof User; // "id" | "name" | "email"

Indexed access types (T[K]) look up the type of a property:

typescript
type IdType = User["id"];   // number
type NameType = User["name"]; // string

// Dynamic lookup
function get<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

Combined with typeof to derive types from values:

typescript
const config = {
  port: 3000,
  host: "localhost",
  debug: false,
} as const;

type Config = typeof config;
type ConfigKey = keyof Config; // "port" | "host" | "debug"
type PortType = Config["port"]; // 3000 (literal, because of as const)

Indexed access with arrays:

typescript
const routes = ["home", "about", "contact"] as const;
type Route = typeof routes[number]; // "home" | "about" | "contact"

28. How does `as const` work and when should you use it?

Answer:

as const is a const assertion that widens nothing—TypeScript infers the narrowest literal types for the entire object tree and marks all properties readonly:

typescript
// Without as const
const config = { env: "production", debug: false };
// { env: string; debug: boolean }—too wide

// With as const
const config = { env: "production", debug: false } as const;
// { readonly env: "production"; readonly debug: false }—literal types

Primary use cases:

  1. 1Deriving union types from arrays:
typescript
const STATUSES = ["pending", "active", "closed"] as const;
type Status = typeof STATUSES[number]; // "pending" | "active" | "closed"
  1. 2Enum alternatives:
typescript
const Direction = {
  Up: "UP",
  Down: "DOWN",
  Left: "LEFT",
  Right: "RIGHT",
} as const;

type Direction = typeof Direction[keyof typeof Direction]; // "UP" | "DOWN" | ...
  1. 3Preventing accidental mutation of config objects

29. What is the `enum` type and why do many teams avoid it?

Answer:

TypeScript enums declare a set of named constants:

typescript
enum Status {
  Pending = "PENDING",
  Active = "ACTIVE",
  Closed = "CLOSED",
}

let s: Status = Status.Pending;

Reasons to avoid numeric enums:

  • Numeric enums have reverse mapping at runtime (extra JavaScript output)
  • Numeric enums allow any number to be assigned to an enum variable under certain conditions
  • They emit JavaScript that doesn't tree-shake well

Reasons to prefer as const objects:

  • Zero runtime footprint (no generated code)
  • Composable with mapped types and template literals
  • The type is just a string union—works everywhere without imports
typescript
const STATUS = { Pending: "PENDING", Active: "ACTIVE" } as const;
type Status = typeof STATUS[keyof typeof STATUS]; // "PENDING" | "ACTIVE"

const enum is a special case that inlines values at compile time with no runtime object—but it has limitations with module boundaries and isolatedModules: true, which most modern build tools require.


30. What are abstract classes and when should you use them over interfaces?

Answer:

Abstract classes can provide partial implementations alongside abstract members that subclasses must fulfill:

typescript
abstract class Repository<T> {
  abstract findById(id: string): Promise<T | null>;
  abstract save(entity: T): Promise<T>;

  async findOrFail(id: string): Promise<T> {
    const entity = await this.findById(id);
    if (!entity) throw new Error(`Entity ${id} not found`);
    return entity;
  }
}

class UserRepository extends Repository<User> {
  async findById(id: string) { /* database query */ return null; }
  async save(user: User) { /* database insert */ return user; }
  // findOrFail is inherited—no need to reimplement
}

Use abstract classes when:

  • You have shared implementation logic that belongs in the base
  • You want to enforce a template-method pattern
  • There's a natural inheritance hierarchy

Use interfaces when:

  • You only need to describe a contract
  • Multiple unrelated classes need to fulfill the same contract
  • You want to avoid locking into a class hierarchy

31. How does TypeScript's control flow analysis work?

Answer:

TypeScript performs static analysis of every code path through a function to track what type a variable must be at each point. This is called control flow analysis (CFA):

typescript
function process(x: string | null) {
  if (x === null) {
    return; // TypeScript knows x is null here
  }
  // TypeScript knows x is string here (null was eliminated above)
  console.log(x.toUpperCase()); // no error
}

CFA handles: if/else, switch, ternary operators, return/throw/break, logical operators (&&, ||, ??), and assignment.

Type guard functions return type predicates so CFA can track narrowing across function calls:

typescript
function isError(x: unknown): x is Error {
  return x instanceof Error;
}

function handle(x: unknown) {
  if (isError(x)) {
    x.message; // Error—CFA followed the predicate
  }
}

32. What are intersection types and how do they compose?

Answer:

An intersection type A & B describes a value that is both A and B simultaneously. Every property of every constituent must be present:

typescript
type Serializable = { serialize(): string };
type Loggable = { log(msg: string): void };

type Service = Serializable & Loggable;
// Must have both serialize() and log()

Common patterns:

Mixin types:

typescript
type WithTimestamps<T> = T & { createdAt: Date; updatedAt: Date };
type UserWithTimestamps = WithTimestamps<User>;

Branded/tagged types:

typescript
type UUID = string & { readonly __brand: "UUID" };

When properties conflict: If both sides define the same property with incompatible types, the result is never for that property—which usually means the intersection is effectively never:

typescript
type A = { x: string };
type B = { x: number };
type C = A & B; // { x: string & number } = { x: never }

Tier 3: Senior / Advanced (Questions 33–47)


33. What is covariance and contravariance in TypeScript?

What is being tested: Deep understanding of the type system. This is a strong signal question for senior roles.

Answer:

Variance describes how type relationships of composite types relate to the type relationships of their components.

Covariance: Subtype direction is preserved. If Dog extends Animal, then Producer is a subtype of Producer. Types in *output* (return) positions are covariant:

typescript
type Producer<T> = () => T;

let makeDog: Producer<Dog> = () => new Dog();
let makeAnimal: Producer<Animal> = makeDog; // OK—covariant

Contravariance: Subtype direction is reversed. If Dog extends Animal, then Consumer is a subtype of Consumer. Types in *input* (parameter) positions are contravariant:

typescript
type Consumer<T> = (x: T) => void;

let handleAnimal: Consumer<Animal> = (a) => console.log(a.name);
let handleDog: Consumer<Dog> = handleAnimal; // OK—contravariant
// handleAnimal = handleDog; // Error—Dog-specific operations might fail on plain Animal

Why contravariance is correct for function parameters: If you promise to accept a Dog, you must handle at least Dog. A function that handles any Animal is strictly more capable—it can be used wherever a dog-handler is expected.

TypeScript enforcement: The strictFunctionTypes flag (part of strict) enforces contravariant parameter checking for function types written as function types. Method shorthand (foo()) is *bivariant* for backward compatibility:

typescript
interface WithMethod {
  handle(x: Dog): void; // bivariant (method shorthand)
}

interface WithProperty {
  handle: (x: Dog) => void; // strictly contravariant
}

TypeScript 4.7+ variance annotations let you explicitly mark type parameters:

typescript
type Producer<out T> = () => T;  // explicitly covariant
type Consumer<in T> = (x: T) => void; // explicitly contravariant

34. What are branded types and when should you use them?

Answer:

Because TypeScript uses structural typing, two string values with different semantic meanings are mutually assignable:

typescript
type UserID = string;
type OrderID = string;

function getUser(id: UserID) { /* ... */ }
const orderId: OrderID = "order-123";
getUser(orderId); // No error—but semantically wrong

Branded types attach a phantom type tag to create structurally distinct types:

typescript
type Brand<T, Tag> = T & { readonly __brand: Tag };

type UserID = Brand<string, "UserID">;
type OrderID = Brand<string, "OrderID">;

function createUserID(id: string): UserID {
  return id as UserID;
}

function getUser(id: UserID): User { /* ... */ }

const userId = createUserID("user-123");
const orderId = "order-456" as OrderID;

getUser(userId);  // OK
getUser(orderId); // Error: OrderID is not assignable to UserID

Use cases: Domain IDs, validated strings (email, URL), unit-typed numbers (USD vs EUR, pixels vs rem), non-negative numbers.

Note: unique symbol provides stronger branding at the cost of more complex code:

typescript
declare const __brand: unique symbol;
type Brand<T, B> = T & { [__brand]: B };

35. What are recursive types and what are their limits?

Answer:

Recursive types reference themselves in their own definition:

typescript
type JSONValue =
  | string
  | number
  | boolean
  | null
  | JSONValue[]
  | { [key: string]: JSONValue };

type TreeNode<T> = {
  value: T;
  children: TreeNode<T>[];
};

type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};

Real-world use: File system trees, AST nodes, nested form structures, menu hierarchies.

Limits:

  1. 1Infinite expansion: TypeScript has a recursion depth limit. Deeply recursive generic types can hit it and produce an error
  2. 2Type-level computation: Recursive conditional types can be very slow to check in large codebases
  3. 3Circular utility types: type A = { child: A } is valid, but type A = A[] is not—the latter doesn't terminate

Practical tip: For deeply recursive structures, define an interface instead of a type alias—TypeScript is more lenient with interface recursion:

typescript
interface TreeNode<T> {
  value: T;
  children: TreeNode<T>[];
}

36. What are variadic tuple types?

Answer:

Variadic tuple types (TypeScript 4.0+) allow a spread of a type parameter in a tuple position, enabling powerful type-level manipulation of argument lists:

typescript
type Concat<T extends unknown[], U extends unknown[]> = [...T, ...U];

type AB = Concat<[string, number], [boolean, string]>;
// [string, number, boolean, string]

Practical application—type-safe function composition:

typescript
function concat<T extends unknown[], U extends unknown[]>(
  a: readonly [...T],
  b: readonly [...U]
): [...T, ...U] {
  return [...a, ...b];
}

const result = concat([1, 2] as const, ["a", "b"] as const);
// [1, 2, "a", "b"] — types fully preserved

Prepend/append to argument lists:

typescript
type Prepend<T, Tuple extends unknown[]> = [T, ...Tuple];
type Append<Tuple extends unknown[], T> = [...Tuple, T];

type WithLogger<Fn extends (...args: any[]) => any> =
  (...args: [...Parameters<Fn>, logger: Console]) => ReturnType<Fn>;

37. How do you build a type-safe event emitter?

What is being tested: The ability to compose advanced type features to solve a real-world problem.

Answer:

typescript
type EventMap = {
  "user:created": { userId: string; name: string };
  "user:deleted": { userId: string };
  "order:placed": { orderId: string; total: number };
};

class TypedEventEmitter<Events extends Record<string, unknown>> {
  private listeners: {
    [K in keyof Events]?: Array<(payload: Events[K]) => void>;
  } = {};

  on<K extends keyof Events>(
    event: K,
    listener: (payload: Events[K]) => void
  ): void {
    if (!this.listeners[event]) {
      this.listeners[event] = [];
    }
    this.listeners[event]!.push(listener);
  }

  emit<K extends keyof Events>(event: K, payload: Events[K]): void {
    this.listeners[event]?.forEach((l) => l(payload));
  }

  off<K extends keyof Events>(
    event: K,
    listener: (payload: Events[K]) => void
  ): void {
    this.listeners[event] = this.listeners[event]?.filter(
      (l) => l !== listener
    );
  }
}

const emitter = new TypedEventEmitter<EventMap>();

emitter.on("user:created", ({ userId, name }) => {
  console.log(`User ${name} created with ID ${userId}`);
});

emitter.emit("user:created", { userId: "1", name: "Alice" }); // OK
emitter.emit("user:created", { userId: "1" }); // Error: missing 'name'
emitter.emit("unknown:event", {}); // Error: not a valid event

38. How do you type a deep `Partial` (all nested levels optional)?

Answer:

The built-in Partial only makes the top level optional. For nested structures:

typescript
type DeepPartial<T> = T extends object
  ? { [K in keyof T]?: DeepPartial<T[K]> }
  : T;

interface Config {
  server: {
    host: string;
    port: number;
    tls: {
      enabled: boolean;
      cert: string;
    };
  };
  database: {
    url: string;
    poolSize: number;
  };
}

type PartialConfig = DeepPartial<Config>;

const update: PartialConfig = {
  server: {
    tls: { enabled: true }, // Only updating one nested field
  },
};

Careful with primitives: The T extends object check prevents recursing into string, number, etc., which would be incorrect.


39. How do you create an `Immutable<T>` that deep-freezes the type?

Answer:

typescript
type Immutable<T> = T extends (infer U)[]
  ? ReadonlyArray<Immutable<U>>
  : T extends Map<infer K, infer V>
  ? ReadonlyMap<Immutable<K>, Immutable<V>>
  : T extends Set<infer U>
  ? ReadonlySet<Immutable<U>>
  : T extends object
  ? { readonly [K in keyof T]: Immutable<T[K]> }
  : T;

interface State {
  users: { id: string; name: string }[];
  settings: Map<string, string>;
}

type FrozenState = Immutable<State>;
// {
//   readonly users: ReadonlyArray<{ readonly id: string; readonly name: string }>;
//   readonly settings: ReadonlyMap<string, string>;
// }

40. What is the `Extract` / `Exclude` pattern and how do you derive subset unions?

Answer:

Extract and Exclude operate on union members the way filter operates on arrays:

typescript
type AllEvents = "click" | "focus" | "blur" | "keydown" | "keyup" | "mouseenter";

type KeyboardEvents = Extract<AllEvents, `key${string}`>;
// "keydown" | "keyup"

type NonKeyboardEvents = Exclude<AllEvents, `key${string}`>;
// "click" | "focus" | "blur" | "mouseenter"

Filtering object union types:

typescript
type Action =
  | { type: "FETCH"; url: string }
  | { type: "SAVE"; data: unknown }
  | { type: "RESET" };

type AsyncActions = Extract<Action, { url: string } | { data: unknown }>;
// { type: "FETCH"; url: string } | { type: "SAVE"; data: unknown }

Building discriminated sub-unions by discriminant:

typescript
type ByType<T extends { type: string }, K extends T["type"]> = Extract<T, { type: K }>;

type FetchAction = ByType<Action, "FETCH">; // { type: "FETCH"; url: string }

41. How do you type React component props with mutually exclusive options?

Answer:

When a component should accept *either* A or B, but *not both*, use discriminated unions with never:

typescript
type BaseButtonProps = {
  children: React.ReactNode;
  disabled?: boolean;
};

type LinkButtonProps = BaseButtonProps & {
  href: string;
  onClick?: never; // explicitly disallow
};

type ActionButtonProps = BaseButtonProps & {
  onClick: () => void;
  href?: never; // explicitly disallow
};

type ButtonProps = LinkButtonProps | ActionButtonProps;

function Button(props: ButtonProps) {
  if (props.href) {
    return <a href={props.href}>{props.children}</a>;
  }
  return <button onClick={props.onClick}>{props.children}</button>;
}

// Usage
<Button href="/home">Go home</Button>          // OK
<Button onClick={() => {}}>Click me</Button>   // OK
<Button href="/home" onClick={() => {}}>Both</Button> // Error

42. Explain how TypeScript handles `this` in classes and how to type it correctly.

Answer:

TypeScript models this as an implicit parameter. You can annotate it explicitly to catch incorrect call contexts:

typescript
interface Clickable {
  handleClick(this: HTMLElement, event: MouseEvent): void;
}

Polymorphic this enables fluent interfaces / method chaining with inheritance:

typescript
class QueryBuilder {
  protected conditions: string[] = [];

  where(condition: string): this {
    this.conditions.push(condition);
    return this; // returns the actual subclass type, not QueryBuilder
  }
}

class UserQueryBuilder extends QueryBuilder {
  orderByName(): this {
    // ...
    return this;
  }
}

const query = new UserQueryBuilder()
  .where("active = true")
  .orderByName() // UserQueryBuilder—not just QueryBuilder
  .where("age > 18");

43. How do you implement a type-safe builder pattern?

Answer:

The builder pattern becomes particularly powerful in TypeScript when you track what has been configured at the type level:

typescript
type BuilderState = {
  name: boolean;
  age: boolean;
  email: boolean;
};

class PersonBuilder<State extends BuilderState = { name: false; age: false; email: false }> {
  private data: Partial<{ name: string; age: number; email: string }> = {};

  setName(name: string): PersonBuilder<State & { name: true }> {
    this.data.name = name;
    return this as any;
  }

  setAge(age: number): PersonBuilder<State & { age: true }> {
    this.data.age = age;
    return this as any;
  }

  setEmail(email: string): PersonBuilder<State & { email: true }> {
    this.data.email = email;
    return this as any;
  }

  build(
    this: PersonBuilder<{ name: true; age: true; email: true }>
  ): { name: string; age: number; email: string } {
    return this.data as any;
  }
}

const person = new PersonBuilder()
  .setName("Alice")
  .setAge(30)
  .setEmail("alice@example.com")
  .build(); // OK

new PersonBuilder().setName("Alice").build(); // Error: age and email not set

44. What TypeScript features affect compile performance, and how do you address them?

Answer:

Features that are expensive to typecheck:

  1. 1Large conditional types with deep recursion: Each instantiation of a complex conditional type is re-evaluated. Prefer mapped types or utility types when they achieve the same result
  2. 2Excessive type assertions: as any cascades through inference, forcing re-evaluation
  3. 3typeof on large objects in hot paths: Inferring the type of a large as const object repeatedly is expensive
  4. 4Barrel files (re-exporting everything): Forces the compiler to load many modules even for small imports

Mitigation strategies:

typescript
// Expensive: recursive conditional type
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};

// Cheaper for known shallow structures: direct mapping
type ShallowReadonly<T> = Readonly<T>;

Compiler flags for performance:

  • skipLibCheck: true — skips type-checking of .d.ts files (huge for large node_modules)
  • incremental: true — caches compilation state between builds
  • composite: true — enables project references for monorepo builds

Type-level performance checks:

typescript
// Use TypeScript's --diagnostics flag to find slow types
// tsc --diagnostics
// Look for "Types" count and "Instantiations" in the output

45. What are decorators in TypeScript and what is their current status?

Answer:

Decorators are a meta-programming feature that lets you annotate and modify classes, methods, properties, and parameters using the @expression syntax.

TypeScript 5.0 introduced the ECMAScript Stage 3 decorator standard, replacing the older experimental decorator proposal. The two systems are not compatible.

Modern decorators (TypeScript 5.0+):

typescript
// Class decorator
function sealed(target: typeof MyClass) {
  Object.seal(target);
  Object.seal(target.prototype);
}

@sealed
class MyClass {
  greeting = "hello";
}

// Method decorator
function log(target: unknown, context: ClassMethodDecoratorContext) {
  const methodName = String(context.name);
  return function (this: unknown, ...args: unknown[]) {
    console.log(`Calling ${methodName} with`, args);
    return (target as Function).apply(this, args);
  };
}

class Service {
  @log
  fetchUser(id: string) {
    return fetch(`/users/${id}`);
  }
}

Legacy (experimental) decorators (NestJS, Angular, TypeORM still use these with experimentalDecorators: true):

typescript
function Injectable(): ClassDecorator {
  return (target) => {
    Reflect.defineMetadata("injectable", true, target);
  };
}

@Injectable()
class UserService {
  constructor(private db: Database) {}
}

What to say in an interview: Know both syntaxes exist, which frameworks use which, and that Stage 3 decorators are the direction TypeScript is moving. Most production codebases using decorators heavily (NestJS, Angular) still use the experimental form.


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

Answer:

Phase 1 — Enable coexistence:

json
// tsconfig.json
{
  "compilerOptions": {
    "allowJs": true,
    "checkJs": false,
    "strict": false,
    "outDir": "./dist"
  }
}

Rename *.js to *.ts one file at a time, starting with utility functions that have no dependencies.

Phase 2 — Tighten the net:

  • Enable checkJs: true to get basic JS checking
  • Add @ts-check comments to JS files you haven't renamed yet
  • Fix errors without rewriting logic

Phase 3 — Harden:

json
{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true
  }
}

Phase 4 — Enforce:

  • Add TypeScript to CI: tsc --noEmit
  • Add a lint rule preventing any in new files
  • Require new PRs to be in .ts

Key principle: The goal is progressive, not perfect. Start with types at the boundaries (API responses, function parameters) and let inference handle the interior. Avoid using any as a workaround—use unknown instead and force callers to narrow.


47. What is the difference between `type-only imports` and regular imports, and why does it matter?

Answer:

Type-only imports (import type) tell TypeScript (and the build tool) that the import is used only at the type level and should be completely erased at runtime:

typescript
// Regular import—bundled into output
import { User } from "./types";

// Type-only import—erased at build time
import type { User } from "./types";

Why it matters:

  1. 1isolatedModules: true (required by esbuild, Babel, swc) cannot analyze cross-file type information. If you import a type without import type, these tools may try to emit the import in the output, causing a runtime module-not-found error for declaration-only files
  2. 2Bundle size: Type-only imports guarantee zero runtime footprint
  3. 3Circular dependencies: Type-only imports break runtime circular dependency cycles while preserving type relationships

Modern TypeScript (4.5+) also supports inline type imports:

typescript
import { type User, createUser } from "./user";
// createUser is a value import; User is erased

Best practice: Add verbatimModuleSyntax: true to your tsconfig to make TypeScript enforce that type imports always use import type—the compiler will error if you use a regular import for a type-only symbol.


Bonus: 3 Design-Level Questions (Asked at Senior+ and Staff Levels)


B1. How would you model a state machine in TypeScript's type system?

typescript
type State = "idle" | "loading" | "success" | "error";
type Event = "FETCH" | "RESOLVE" | "REJECT" | "RESET";

type Transitions = {
  idle: { FETCH: "loading" };
  loading: { RESOLVE: "success"; REJECT: "error" };
  success: { RESET: "idle" };
  error: { RESET: "idle" };
};

type NextState<
  S extends State,
  E extends keyof Transitions[S]
> = Transitions[S][E];

// TypeScript prevents invalid transitions at compile time
type A = NextState<"idle", "FETCH">;   // "loading"
type B = NextState<"idle", "RESET">;   // Error: "RESET" is not a key of Transitions["idle"]

B2. How do you prevent `Object.keys` from losing type information?

TypeScript widens Object.keys(obj) to string[] because objects can have more keys at runtime than their type declares. This is correct but inconvenient:

typescript
// Typed wrapper using keyof
function typedKeys<T extends object>(obj: T): (keyof T)[] {
  return Object.keys(obj) as (keyof T)[];
}

const user = { name: "Alice", age: 30 };
for (const key of typedKeys(user)) {
  // key: "name" | "age"—not string
  console.log(user[key]);
}

Important caveat to state in an interview: This is safe only when you control the object and know it has no extra properties. Accepting T extends object from an outside source may have extra keys, and the assertion can hide bugs.


B3. How would you type a `pipe` / `compose` function?

typescript
function pipe<A>(a: A): A;
function pipe<A, B>(a: A, ab: (a: A) => B): B;
function pipe<A, B, C>(a: A, ab: (a: A) => B, bc: (b: B) => C): C;
function pipe<A, B, C, D>(
  a: A,
  ab: (a: A) => B,
  bc: (b: B) => C,
  cd: (c: C) => D
): D;
// ... (overloads continue as needed)
function pipe(a: unknown, ...fns: Array<(x: unknown) => unknown>): unknown {
  return fns.reduce((acc, fn) => fn(acc), a);
}

const result = pipe(
  "  hello world  ",
  (s) => s.trim(),          // string
  (s) => s.split(" "),      // string[]
  (arr) => arr.length       // number
);
// result: number

Libraries like fp-ts use variadic tuple types and conditional types to implement pipe with unlimited arity—worth studying if you work with functional TypeScript.


Common Mistakes That Sink Interviews

1. Using any when unknown is correct. Any time you receive external data (API, JSON, user input), the honest type is unknown. any says "I know this is fine." unknown says "I'll prove it."

2. Using as to paper over type errors. Type assertions should reflect facts you know, not silence errors you don't understand.

3. Confusing interface and type in the wrong direction. Saying "interfaces are for objects, types are for everything else" is incomplete. The real answer involves mergeability and expressiveness.

4. Thinking TypeScript prevents runtime errors. TypeScript checks types at compile time. After compilation, you have JavaScript. Type assertions, any, and external data can all introduce runtime type errors.

5. Not knowing what strict: true does. Every TypeScript engineer should be able to list at least strictNullChecks, noImplicitAny, and strictFunctionTypes from memory.

6. Treating enums as the only option for constants. Know the tradeoffs between enum, const enum, and as const objects.

7. Writing overloads when a generic would be cleaner. Overloads are for when the relationship between argument and return types is *non-parametric*. If it's consistent, generics are better.


Quick Reference: TypeScript Version Features

| Feature | Version |

|---------|---------|

| Template literal types | 4.1 |

| Key remapping in mapped types | 4.1 |

| Variadic tuple types | 4.0 |

| satisfies operator | 4.9 |

| Variance annotations (in/out) | 4.7 |

| const type parameter modifier | 5.0 |

| ECMAScript Stage 3 decorators | 5.0 |

| verbatimModuleSyntax | 5.0 |

| using keyword (explicit resource management) | 5.2 |

FAQ

What is the difference between `interface` and `type` in TypeScript?+

The key behavioral difference is that interfaces support declaration merging (two interfaces with the same name are automatically combined) while type aliases do not. Type aliases can describe unions, tuples, and primitives; interfaces cannot. For public API shapes that consumers might extend, prefer interface. For unions, tuples, or cases where you want to prevent accidental extension, prefer type.

What does `strict: true` enable in tsconfig.json?+

It enables a group of flags: strictNullChecks (null and undefined are distinct types), noImplicitAny (no implicit any on parameters), strictFunctionTypes (contravariant parameter checking), strictBindCallApply (type-checked bind/call/apply), strictPropertyInitialization (class properties must be initialized), noImplicitThis, and useUnknownInCatchVariables. The single most impactful flag is strictNullChecks.

What is the difference between `any`, `unknown`, and `never`?+

`any` disables type checking entirely. `unknown` accepts any value but requires you to narrow it before using it—it is the type-safe counterpart to `any`. `never` represents values that can never exist: it appears at the bottom of exhaustive union checks and as the return type of functions that always throw. Mental model: any = anything, no rules; unknown = anything, prove it first; never = nothing can ever be here.

What is the `infer` keyword and how is it used?+

`infer` is used inside conditional types to capture and name a type that TypeScript infers from the matched structure. For example, `type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never` extracts the return type of any function by letting TypeScript infer what `R` is in the return position. It only works inside the `extends` clause of a conditional type.

What is covariance and contravariance in TypeScript?+

Variance describes how subtype relationships of composite types relate to their component types. Covariance (output/return positions) preserves the subtype direction: if Dog extends Animal, then () => Dog is a subtype of () => Animal. Contravariance (input/parameter positions) reverses the direction: (x: Animal) => void is a subtype of (x: Dog) => void, because a function that handles any Animal is strictly more capable. The `strictFunctionTypes` flag enforces this for function type expressions.

What is the `satisfies` operator and when should you use it?+

`satisfies` (TypeScript 4.9+) validates that a value matches a type without widening the inferred type to that type. Before `satisfies`, annotating a value as `Record<Palette, string | [number, number, number]>` caused TypeScript to forget whether individual values were strings or arrays. With `satisfies`, TypeScript validates completeness while preserving the precise literal types of each property. Use it for config objects, design tokens, route maps, and anywhere you need both validation and literal-type precision.

What are branded types and why are they needed?+

Because TypeScript uses structural typing, two `string` values with different semantic meanings (UserID and OrderID) are mutually assignable. Branded types attach a phantom type tag to create structurally distinct types at compile time with zero runtime overhead: `type UserID = string & { readonly __brand: 'UserID' }`. This prevents accidentally passing an OrderID where a UserID is expected, simulating nominal typing in a structural type system.

How do discriminated unions differ from class hierarchies for modeling state?+

Discriminated unions are plain objects with a shared literal discriminant property—no `new`, no `instanceof`, no runtime inheritance. They work with pure TypeScript exhaustiveness checking (the `never` trick), are serializable by default, tree-shake cleanly, and model impossible states better than class flags. Class hierarchies are appropriate when you need shared implementation logic and polymorphic dispatch, but for modeling UI state, API responses, or domain states, discriminated unions are almost always the cleaner choice.

Artículos relacionados

How to Answer Conflict-With-a-Coworker Interview Questions

Learn how to answer conflict-with-a-coworker interview questions with real examples and proven techniques. Stand out in tech and remote job interviews.

How to Answer 'Why Do You Want to Work Here' in Interviews

Discover expert strategies for answering 'why do you want to work here,' tailored for remote tech roles and dollar opportunities. Real, practical interview tips.

Angular Developer Interview Questions — 40 with Code and Answers

The 40 Angular interview questions that actually get asked at real companies: dependency injection, RxJS, change detection, lazy loading, signals. Detailed answers with TypeScript code.

Frontend Developer Interview Questions and How to Answer Them (50+)

Complete SEO article covering 54 frontend developer interview questions with detailed answers, real code snippets across HTML, CSS, JavaScript, React, TypeScript, accessibility, security, build tools, and testing.

Preparate para tu entrevista real

Pegá el link de tu vacante: investigamos quién te entrevista y te ensayamos en vivo.

Empezar gratis →

¿Tenés entrevista próxima? Instalá el copiloto en vivo →

InterviewHack.ai

Preparate para la entrevista exacta: quién te entrevista, tu CV a medida y coach real.

Producto

VacantesRevisar CV (ATS) gratis¿Cómo suena tu inglés?¿Te pagan bien?Reporte de sueldos LATAMCursos gratisBlogCV a medidaPráctica habladaEs gratis

Empleos remotos

ReactPythonFull-StackLATAMArgentinaMéxicoVer todas →

Preparate

Práctica habladaFrontendBackendAI EngineerPor empresaVendete con tu CV

Empresa

Buscás talentoAcerca deContactoPrivacidadTérminos

© 2026 InterviewHack.ai · Tu CV es tuyo. Nunca se usa para entrenar nada. · Un producto de IA-PTY