Toolmingo
Guides27 min read

50 TypeScript Interview Questions (With Answers)

Top TypeScript interview questions with clear answers and code examples — covering types, generics, utility types, decorators, narrowing, and advanced patterns.

TypeScript interviews test type system mastery, structural typing, generics, compiler configuration, and real-world patterns. This guide covers the 50 most common questions — with concise answers and runnable examples.

Quick reference

Topic Most asked questions
Type system any vs unknown vs never, type vs interface
Narrowing typeof, instanceof, discriminated unions
Generics constraints, conditional types, infer
Utility types Partial, Required, Pick, Omit, Record, ReturnType
Decorators class, method, property decorators
Config strict mode, tsconfig options, module systems
Patterns mapped types, template literal types, branded types

Type system fundamentals

1. What is the difference between type and interface?

Both declare shapes for objects, but they differ in extensibility and capability.

// interface — can be extended, merged (declaration merging)
interface User {
  id: number;
  name: string;
}
interface User {           // declaration merging — valid
  email: string;
}
interface Admin extends User {
  role: "admin";
}

// type — cannot be merged, more expressive
type User = { id: number; name: string };
type StringOrNumber = string | number;  // union — only possible with type
type Tuple = [string, number];           // tuple alias

Rule of thumb: use interface for public API shapes (extensible by consumers), use type for unions, intersections, mapped types, and aliases.


2. What is any vs unknown vs never?

Type Assignable to anything? Assignable from anything? Use case
any Yes Yes Escape hatch — disables type checking
unknown No (must narrow first) Yes Safe "I don't know yet"
never Yes No Unreachable code, exhaustive checks
let a: any = 42;
a.toUpperCase();   // no error — dangerous

let u: unknown = 42;
u.toUpperCase();   // Error — must narrow first
if (typeof u === "string") u.toUpperCase();  // OK

function fail(msg: string): never {
  throw new Error(msg);  // never returns
}

// never for exhaustive checks
type Shape = "circle" | "square";
function area(s: Shape) {
  if (s === "circle") return Math.PI;
  if (s === "square") return 1;
  const _exhaustive: never = s;  // error if new Shape added
}

3. What is structural typing?

TypeScript uses structural (duck) typing — two types are compatible if they have the same structure, regardless of name.

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

const p: Point = { x: 1, y: 2 };
const c: Coord = p;  // OK — structurally identical

class Cat { meow() {} }
class Dog { meow() {} }

const cat: Cat = new Dog();  // OK — same structure!

Contrast with nominal typing (Java/C#) where Cat and Dog are incompatible even if identical.


4. What is type narrowing?

TypeScript narrows a broad type to a narrower one inside a conditional block.

function format(value: string | number) {
  if (typeof value === "string") {
    return value.toUpperCase();  // string here
  }
  return value.toFixed(2);       // number here
}

// instanceof narrowing
function getArea(shape: Circle | Square) {
  if (shape instanceof Circle) {
    return Math.PI * shape.radius ** 2;
  }
  return shape.side ** 2;
}

// in operator narrowing
type Fish = { swim: () => void };
type Bird = { fly: () => void };
function move(animal: Fish | Bird) {
  if ("swim" in animal) animal.swim();
  else animal.fly();
}

5. What are discriminated unions?

A union where each member has a common literal field (the discriminant) used to narrow.

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; side: number }
  | { kind: "rectangle"; width: number; height: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":    return Math.PI * shape.radius ** 2;
    case "square":    return shape.side ** 2;
    case "rectangle": return shape.width * shape.height;
    default:
      const _never: never = shape;  // exhaustive check
      return _never;
  }
}

Discriminated unions are the TypeScript equivalent of tagged unions / algebraic data types.


6. What are literal types and const assertions?

Literal types narrow a value to its exact type instead of a broad primitive.

let x = "hello";         // type: string
const y = "hello";       // type: "hello" (literal)

type Direction = "north" | "south" | "east" | "west";
function go(d: Direction) {}
go("north");   // OK
go("up");      // Error

// const assertion — freeze an object's types
const config = {
  endpoint: "https://api.example.com",
  retries: 3,
} as const;
// config.endpoint: "https://api.example.com" (literal, not string)
// config.retries: 3 (literal, not number)

type Config = typeof config;  // { readonly endpoint: "https://api.example.com"; readonly retries: 3 }

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

Type assertions (as T or <T>) tell the compiler "I know better" — they do not change runtime behaviour.

const input = document.getElementById("name") as HTMLInputElement;
input.value = "Alice";  // OK — we know it's an input

// double assertion (escape hatch — avoid if possible)
const x = "hello" as unknown as number;

// satisfies operator (TS 4.9+) — better than assertion
const config = {
  port: 3000,
  host: "localhost",
} satisfies Record<string, string | number>;
// config.port is still 3000 (number), not string | number

Use assertions only when you have information the compiler lacks (DOM APIs, JSON parsing). Prefer type guards for runtime safety.


Generics

8. What are generics and why are they useful?

Generics allow writing reusable, type-safe code parameterised by type.

// Without generics
function identity(arg: any): any { return arg; }

// With generics — preserves type
function identity<T>(arg: T): T { return arg; }

const s = identity("hello");  // s: string
const n = identity(42);       // n: number

// Generic interface
interface Repository<T> {
  findById(id: number): Promise<T>;
  save(entity: T): Promise<T>;
  delete(id: number): Promise<void>;
}

// Generic class
class Stack<T> {
  private items: T[] = [];
  push(item: T) { this.items.push(item); }
  pop(): T | undefined { return this.items.pop(); }
}

9. What are generic constraints?

Constraints limit which types a generic can accept using extends.

// T must have a length property
function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}
longest("alice", "bob");       // OK
longest([1, 2, 3], [1, 2]);    // OK
longest(10, 20);               // Error — number has no length

// keyof constraint
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}
const user = { name: "Alice", age: 30 };
getProperty(user, "name");  // string
getProperty(user, "age");   // number
getProperty(user, "email"); // Error

10. What are conditional types?

Conditional types (T extends U ? X : Y) choose a type based on a condition.

type IsString<T> = T extends string ? "yes" : "no";
type A = IsString<string>;  // "yes"
type B = IsString<number>;  // "no"

// Practical: extract return type
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type R = ReturnType<() => string>;  // string

// Distributive conditional types
type Flatten<T> = T extends Array<infer Item> ? Item : T;
type F1 = Flatten<string[]>;   // string
type F2 = Flatten<number[][]>; // number[]
type F3 = Flatten<string>;     // string

// NonNullable
type NonNullable<T> = T extends null | undefined ? never : T;
type N = NonNullable<string | null | undefined>;  // string

11. What is the infer keyword?

infer declares a type variable inside a conditional type to capture part of a matched type.

// Extract first argument type
type FirstArg<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;
type F = FirstArg<(x: string, y: number) => void>;  // string

// Extract Promise value
type Awaited<T> = T extends Promise<infer V> ? V : T;
type A = Awaited<Promise<string>>;  // string

// Recursive Awaited (TS 4.5+ built-in)
type DeepAwaited<T> = T extends Promise<infer V> ? DeepAwaited<V> : T;
type D = DeepAwaited<Promise<Promise<number>>>;  // number

// Extract array element type
type ElementType<T extends any[]> = T extends Array<infer E> ? E : never;

Utility types

12. What utility types does TypeScript provide?

TypeScript ships built-in utility types that transform existing types.

interface User {
  id: number;
  name: string;
  email: string;
  age?: number;
}

// Partial — all properties optional
type PartialUser = Partial<User>;

// Required — all properties required
type RequiredUser = Required<User>;

// Pick — select properties
type UserSummary = Pick<User, "id" | "name">;

// Omit — exclude properties
type UserWithoutEmail = Omit<User, "email">;

// Record — key-value map
type UserMap = Record<string, User>;

// Readonly — freeze
type FrozenUser = Readonly<User>;

// Exclude / Extract — for unions
type A = Exclude<"a" | "b" | "c", "a">;         // "b" | "c"
type B = Extract<"a" | "b" | "c", "a" | "d">;   // "a"

// ReturnType / Parameters
type R = ReturnType<() => string>;           // string
type P = Parameters<(x: number, y: string) => void>;  // [number, string]

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

// Awaited (TS 4.5+)
type V = Awaited<Promise<number>>;  // number

13. What are mapped types?

Mapped types transform every property of an existing type.

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

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

// Nullable — make every property nullable
type Nullable<T> = {
  [K in keyof T]: T[K] | null;
};

// Modifiers: + adds, - removes
type Mutable<T> = {
  -readonly [K in keyof T]: T[K];  // remove readonly
};
type AllRequired<T> = {
  [K in keyof T]-?: T[K];  // remove optional
};

// Remap keys (TS 4.1+)
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
// Getters<{ name: string }> → { getName: () => string }

14. What are template literal types?

Template literal types create string types by combining literal types.

type EventName = "click" | "focus" | "blur";
type Handler = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur"

type CSSUnit = "px" | "em" | "rem";
type CSSValue = `${number}${CSSUnit}`;
// "0px" | "10em" | ...

// Practical: typed event emitter keys
type DBEvent = "user" | "post";
type DBAction = "created" | "updated" | "deleted";
type EventKey = `${DBEvent}:${DBAction}`;
// "user:created" | "user:updated" | "user:deleted" | "post:created" | ...

function on(event: EventKey, handler: () => void) {}
on("user:created", () => {});   // OK
on("user:removed", () => {});   // Error

Advanced patterns

15. What are decorators in TypeScript?

Decorators are functions that modify classes, methods, properties, or parameters at design time. Require experimentalDecorators: true in tsconfig.

// Method decorator
function log(target: any, key: string, descriptor: PropertyDescriptor) {
  const original = descriptor.value;
  descriptor.value = function (...args: any[]) {
    console.log(`Calling ${key} with`, args);
    const result = original.apply(this, args);
    console.log(`${key} returned`, result);
    return result;
  };
  return descriptor;
}

class Calculator {
  @log
  add(a: number, b: number) { return a + b; }
}

// Class decorator
function singleton<T extends { new(...args: any[]): {} }>(ctor: T) {
  let instance: InstanceType<T>;
  return class extends ctor {
    constructor(...args: any[]) {
      if (instance) return instance;
      super(...args);
      instance = this as any;
    }
  };
}

@singleton
class AppConfig { port = 3000; }

16. What are branded (nominal) types?

TypeScript is structural, but you can simulate nominal typing with brands to prevent misuse of primitive types.

// Without branding — easy to mix up
function transfer(from: number, to: number, amount: number) {}
transfer(userId, accountId, amount);  // which is which?

// With branded types
type UserId = number & { readonly _brand: "UserId" };
type AccountId = number & { readonly _brand: "AccountId" };
type Amount = number & { readonly _brand: "Amount" };

function brand<T, B extends string>(value: T): T & { readonly _brand: B } {
  return value as any;
}

const userId = brand<number, "UserId">(1);
const accountId = brand<number, "AccountId">(2);
const amount = brand<number, "Amount">(100);

function transfer(from: UserId, to: AccountId, amount: Amount) {}
transfer(userId, accountId, amount);   // OK
transfer(accountId, userId, amount);   // Error — brands don't match

17. What is the satisfies operator (TypeScript 4.9+)?

satisfies validates that a value matches a type without widening it.

type ColorMap = Record<string, [number, number, number] | string>;

// Problem with type annotation — loses literal types
const palette: ColorMap = {
  red: [255, 0, 0],
  green: "#00ff00",
};
palette.red;    // [number, number, number] | string — too wide

// satisfies — validates AND preserves narrow type
const palette = {
  red: [255, 0, 0],
  green: "#00ff00",
} satisfies ColorMap;

palette.red;    // [number, number, number] — narrow!
palette.green;  // string — narrow!
palette.red.map(c => c * 2);  // OK — TypeScript knows it's an array

18. What is a type predicate / user-defined type guard?

A function that returns x is T narrows the type for callers.

interface Fish { swim(): void; }
interface Bird { fly(): void; }

// Type predicate
function isFish(animal: Fish | Bird): animal is Fish {
  return (animal as Fish).swim !== undefined;
}

const animal: Fish | Bird = getAnimal();
if (isFish(animal)) {
  animal.swim();  // TypeScript knows it's Fish here
} else {
  animal.fly();   // TypeScript knows it's Bird here
}

// Practical: API response guard
interface ApiError { error: string; }
interface ApiSuccess<T> { data: T; }

function isError<T>(res: ApiError | ApiSuccess<T>): res is ApiError {
  return "error" in res;
}

19. What is the difference between readonly and const?

  • const applies to variables — the binding cannot be reassigned.
  • readonly applies to properties — the property cannot be reassigned.
const arr = [1, 2, 3];
arr.push(4);       // OK — const prevents reassignment, not mutation
arr = [4, 5, 6];   // Error — cannot reassign const

interface Config {
  readonly host: string;
  readonly port: number;
}
const config: Config = { host: "localhost", port: 3000 };
config.host = "example.com";  // Error — readonly

// ReadonlyArray — immutable array
const nums: ReadonlyArray<number> = [1, 2, 3];
nums.push(4);  // Error
nums[0] = 10;  // Error

// as const — deep readonly
const point = { x: 1, y: 2 } as const;
point.x = 5;  // Error

20. What are index signatures?

Index signatures define types for dynamic property access.

// Allow any string key
interface StringMap {
  [key: string]: string;
}
const map: StringMap = { name: "Alice", city: "NY" };
map.anything = "value";  // OK

// Combine with known properties (must be compatible)
interface Config {
  [key: string]: string | number;
  host: string;    // must match index signature value type
  port: number;    // must match index signature value type
}

// Index signature with template literal keys (TS 4.4+)
interface DataAttrs {
  [key: `data-${string}`]: string;
}

// Record<K, V> is the utility-type equivalent
type Scores = Record<string, number>;

Configuration & compiler

21. What does strict: true enable?

strict is a shorthand for a set of individual strict checks:

Flag What it catches
strictNullChecks null/undefined not assignable to other types
strictFunctionTypes contravariant function parameter checking
strictBindCallApply correct typing for bind/call/apply
strictPropertyInitialization class properties must be initialised
noImplicitAny error on implicit any
noImplicitThis error on this with implicit any
alwaysStrict "use strict" in every file

Always enable strict: true in new projects. It catches entire categories of bugs at compile time.


22. What is the difference between esModuleInterop and allowSyntheticDefaultImports?

  • allowSyntheticDefaultImports — allows import React from "react" when the module has no default export (type checking only).
  • esModuleInterop — emits helper code at runtime to make CommonJS modules work with default imports. Implies allowSyntheticDefaultImports.
// tsconfig.json
{
  "compilerOptions": {
    "esModuleInterop": true,
    "module": "commonjs"
  }
}
// Without esModuleInterop
import * as React from "react";

// With esModuleInterop
import React from "react";  // works

23. What is the difference between target and lib?

  • target — the JavaScript version TypeScript compiles output to (ES5, ES2017, ESNext).
  • lib — which built-in APIs are available (DOM types, Promise, Array methods).
{
  "compilerOptions": {
    "target": "ES2017",  // output uses async/await natively
    "lib": ["ES2017", "DOM"]  // include DOM types + ES2017 APIs
  }
}

Setting target: "ES2017" automatically includes compatible lib entries, but you can override to add DOM types or exclude browser APIs from Node.js projects.


24. What is declaration and declarationMap?

  • declaration: true — generates .d.ts files alongside compiled .js. Required when publishing a library.
  • declarationMap: true — generates .d.ts.map so IDEs can navigate from .d.ts to original .ts source.
{
  "compilerOptions": {
    "declaration": true,
    "declarationMap": true,
    "outDir": "./dist"
  }
}

Functions & classes

25. What is function overloading in TypeScript?

TypeScript allows declaring multiple signatures with a single implementation.

function format(value: string): string;
function format(value: number, decimals: number): string;
function format(value: string | number, decimals?: number): string {
  if (typeof value === "string") return value.trim();
  return value.toFixed(decimals ?? 2);
}

format("hello ");     // OK — string signature
format(3.14159, 2);   // OK — number signature
format(3.14159);      // Error — no matching overload

The last (implementation) signature is not callable from outside — only the overload signatures are visible.


26. What are abstract classes?

Abstract classes cannot be instantiated directly — they serve as base classes.

abstract class Animal {
  abstract makeSound(): string;  // must be implemented by subclass

  move(): void {                 // concrete method — inherited
    console.log("moving...");
  }
}

class Dog extends Animal {
  makeSound(): string { return "woof"; }
}

const a = new Animal();  // Error — cannot instantiate abstract class
const d = new Dog();     // OK
d.makeSound();           // "woof"
d.move();                // "moving..."

Use abstract classes when you want to share implementation between subclasses. Use interfaces when you only need a contract.


27. What is the difference between implements and extends?

  • extends — inherit from a class (single inheritance).
  • implements — promise to satisfy an interface (no implementation inherited).
interface Serializable {
  serialize(): string;
}

class Base {
  log() { console.log("base"); }
}

class Entity extends Base implements Serializable {
  serialize(): string {
    return JSON.stringify(this);
  }
}

// A class can implement multiple interfaces
class Point implements Serializable, Comparable {
  serialize() { return `${this.x},${this.y}`; }
  compareTo(other: Point) { return 0; }
}

28. What are access modifiers?

Modifier Accessible from
public (default) anywhere
protected class + subclasses
private class only (compile-time)
#name class only (runtime — ES private fields)
class BankAccount {
  public owner: string;          // default
  protected balance: number;
  private readonly _id: string;
  #pin: number;                   // true runtime private

  constructor(owner: string, balance: number) {
    this.owner = owner;
    this.balance = balance;
    this._id = crypto.randomUUID();
    this.#pin = 1234;
  }
}

class SavingsAccount extends BankAccount {
  addInterest() {
    this.balance *= 1.05;  // OK — protected
    this._id;              // Error — private
  }
}

Use # private fields for true encapsulation. TypeScript private is erased at runtime.


29. What is constructor parameter shorthand?

TypeScript lets you declare and initialise class fields directly in the constructor.

// Without shorthand
class User {
  public name: string;
  private age: number;
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

// With shorthand
class User {
  constructor(
    public name: string,
    private age: number,
    protected readonly id: string = crypto.randomUUID()
  ) {}
}

Async & advanced

30. How does TypeScript handle Promise and async/await?

TypeScript infers Promise<T> from return types automatically.

async function fetchUser(id: number): Promise<User> {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
  return res.json() as Promise<User>;
}

// Parallel
const [user, posts] = await Promise.all([
  fetchUser(1),
  fetchPosts(1),
]);

// Error handling
async function safeGet<T>(fn: () => Promise<T>): Promise<[T, null] | [null, Error]> {
  try {
    return [await fn(), null];
  } catch (e) {
    return [null, e instanceof Error ? e : new Error(String(e))];
  }
}
const [data, err] = await safeGet(() => fetchUser(1));

31. What is as const and when do you use it?

as const freezes an expression so every value becomes its literal type.

// Without as const
const routes = {
  home: "/",
  about: "/about",
};
// routes.home: string — too wide

// With as const
const routes = {
  home: "/",
  about: "/about",
} as const;
// routes.home: "/" (literal)

type Route = typeof routes[keyof typeof routes];  // "/" | "/about"

// Const enum alternative
const HttpMethod = {
  GET: "GET",
  POST: "POST",
  PUT: "PUT",
  DELETE: "DELETE",
} as const;
type HttpMethod = typeof HttpMethod[keyof typeof HttpMethod];
// "GET" | "POST" | "PUT" | "DELETE"

32. What is the keyof operator?

keyof T produces a union of property name literal types of T.

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

type UserKey = keyof User;  // "id" | "name" | "email"

function pluck<T, K extends keyof T>(obj: T, keys: K[]): T[K][] {
  return keys.map(k => obj[k]);
}
const user: User = { id: 1, name: "Alice", email: "a@b.com" };
pluck(user, ["name", "email"]);  // string[]
pluck(user, ["id"]);             // number[]

// typeof + keyof
const config = { host: "localhost", port: 3000 } as const;
type ConfigKey = keyof typeof config;  // "host" | "port"

33. What is the typeof type operator vs JavaScript typeof?

In type position, typeof extracts the TypeScript type of a value.

const user = { id: 1, name: "Alice" };
type User = typeof user;  // { id: number; name: string }

function add(a: number, b: number) { return a + b; }
type Add = typeof add;  // (a: number, b: number) => number

// Use to derive types from runtime values (single source of truth)
const defaultConfig = {
  retries: 3,
  timeout: 5000,
  debug: false,
} as const;
type Config = typeof defaultConfig;
// { readonly retries: 3; readonly timeout: 5000; readonly debug: false }

34. What are declaration merging and module augmentation?

Declaration merging lets you split an interface across multiple declarations.

interface Window {
  myApp: { version: string };
}
// Now window.myApp is typed everywhere

// Module augmentation — extend a third-party module
declare module "express-serve-static-core" {
  interface Request {
    user?: { id: number; role: string };
  }
}
// req.user is now typed in all Express route handlers

Only interface (not type) supports declaration merging.


35. What is namespace vs ES module?

  • Namespaces — TypeScript-specific way to organise code in global scripts (pre-modules era). Avoid in module-based projects.
  • ES Modules — standard import/export. Use exclusively in modern code.
// Namespace (legacy — global scripts only)
namespace Utils {
  export function clamp(n: number, min: number, max: number) {
    return Math.max(min, Math.min(max, n));
  }
}
Utils.clamp(5, 0, 10);

// ES Module (preferred)
export function clamp(n: number, min: number, max: number) {
  return Math.max(min, Math.min(max, n));
}

Practical patterns

36. How do you type React component props?

import { ReactNode, CSSProperties } from "react";

// Props interface
interface ButtonProps {
  label: string;
  onClick: () => void;
  variant?: "primary" | "secondary";
  disabled?: boolean;
  children?: ReactNode;
  style?: CSSProperties;
}

// Functional component
const Button: React.FC<ButtonProps> = ({ label, onClick, variant = "primary", disabled, children }) => (
  <button className={`btn-${variant}`} onClick={onClick} disabled={disabled}>
    {children ?? label}
  </button>
);

// With generic component
interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => ReactNode;
  keyExtractor: (item: T) => string;
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
  return <ul>{items.map(item => <li key={keyExtractor(item)}>{renderItem(item)}</li>)}</ul>;
}

37. What is the Readonly utility and when should you use it?

interface Config {
  host: string;
  port: number;
  options: string[];
}

function initApp(config: Readonly<Config>) {
  config.host = "other";     // Error — readonly
  config.options.push("x");  // OK — Readonly is shallow!
  // For deep: use DeepReadonly custom type or as const
}

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

Use Readonly<T> for function parameters you don't want to mutate, especially config objects and API responses.


38. How do you type a generic fetch/API client?

async function apiGet<T>(url: string): Promise<T> {
  const res = await fetch(url);
  if (!res.ok) throw new Error(`HTTP ${res.status}: ${url}`);
  return res.json() as Promise<T>;
}

// Usage
const user = await apiGet<User>("/api/users/1");
user.name;  // typed

// With discriminated union result type
type ApiResult<T> = { ok: true; data: T } | { ok: false; error: string };

async function safeFetch<T>(url: string): Promise<ApiResult<T>> {
  try {
    const data = await apiGet<T>(url);
    return { ok: true, data };
  } catch (e) {
    return { ok: false, error: String(e) };
  }
}

const result = await safeFetch<User>("/api/users/1");
if (result.ok) {
  result.data.name;  // User
} else {
  result.error;      // string
}

39. What is the builder pattern in TypeScript?

class QueryBuilder<T> {
  private table = "";
  private conditions: string[] = [];
  private limitVal?: number;

  from(table: string): this {
    this.table = table;
    return this;
  }

  where(condition: string): this {
    this.conditions.push(condition);
    return this;
  }

  limit(n: number): this {
    this.limitVal = n;
    return this;
  }

  build(): string {
    let q = `SELECT * FROM ${this.table}`;
    if (this.conditions.length > 0) q += ` WHERE ${this.conditions.join(" AND ")}`;
    if (this.limitVal !== undefined) q += ` LIMIT ${this.limitVal}`;
    return q;
  }
}

const query = new QueryBuilder()
  .from("users")
  .where("age > 18")
  .where("active = true")
  .limit(10)
  .build();
// SELECT * FROM users WHERE age > 18 AND active = true LIMIT 10

40. How do you create a type-safe event emitter?

type EventMap = {
  userCreated: { id: number; name: string };
  userDeleted: { id: number };
  error: Error;
};

class TypedEmitter<Events extends Record<string, any>> {
  private handlers = new Map<keyof Events, Set<(data: any) => void>>();

  on<K extends keyof Events>(event: K, handler: (data: Events[K]) => void): this {
    if (!this.handlers.has(event)) this.handlers.set(event, new Set());
    this.handlers.get(event)!.add(handler);
    return this;
  }

  off<K extends keyof Events>(event: K, handler: (data: Events[K]) => void): this {
    this.handlers.get(event)?.delete(handler);
    return this;
  }

  emit<K extends keyof Events>(event: K, data: Events[K]): void {
    this.handlers.get(event)?.forEach(h => h(data));
  }
}

const emitter = new TypedEmitter<EventMap>();
emitter.on("userCreated", ({ id, name }) => console.log(id, name));
emitter.emit("userCreated", { id: 1, name: "Alice" });   // OK
emitter.emit("userCreated", { id: 1 });                   // Error — missing name

Common mistakes

41. Why is any dangerous and what should you use instead?

// BAD: any disables type checking entirely
function parse(input: any): any {
  return JSON.parse(input);
}
parse(42).toUpperCase();  // no error — crashes at runtime

// GOOD: unknown forces narrowing
function safeParse(input: unknown): Record<string, unknown> {
  if (typeof input !== "string") throw new TypeError("Expected string");
  const parsed: unknown = JSON.parse(input);
  if (typeof parsed !== "object" || parsed === null) throw new TypeError("Expected object");
  return parsed as Record<string, unknown>;
}

42. What is the void type?

void means a function returns nothing useful — different from undefined.

function log(msg: string): void {
  console.log(msg);
  // implicitly returns undefined
}

// void allows ignoring the return value
type Callback = () => void;
const cb: Callback = () => 42;  // OK — return value is ignored

// undefined is stricter
type Fn = () => undefined;
const fn: Fn = () => 42;  // Error — must return undefined

43. What is the non-null assertion operator !?

! tells TypeScript "this value is never null or undefined" — no runtime check.

// TypeScript doesn't know getElementById returns non-null
const el = document.getElementById("btn");
el.click();   // Error — el could be null

// Non-null assertion (use with care)
const el = document.getElementById("btn")!;
el.click();   // OK — you assert it's not null

// Better: type guard
const el = document.getElementById("btn");
if (!el) throw new Error("Element not found");
el.click();   // OK — narrowed to HTMLElement

44. What is the optional chaining operator ?.?

?. short-circuits to undefined instead of throwing if a value is null or undefined.

interface User {
  address?: { city?: string };
}

const user: User = {};

// Without optional chaining
const city = user.address && user.address.city;

// With optional chaining
const city = user?.address?.city;  // undefined, not an error

// With method calls
const len = str?.length;
const result = arr?.find(x => x > 5);

// Nullish coalescing with optional chaining
const displayCity = user?.address?.city ?? "Unknown";

45. What are enums and what are their drawbacks?

// Numeric enum (default — starts at 0)
enum Direction {
  North,   // 0
  South,   // 1
  East,    // 2
  West,    // 3
}

// String enum (preferred — safe and debuggable)
enum Status {
  Active = "ACTIVE",
  Inactive = "INACTIVE",
  Pending = "PENDING",
}

// const enum — inlined at compile time, no runtime object
const enum Size { Small = 1, Medium = 2, Large = 3 }
const s = Size.Small;  // compiled to: const s = 1;

Drawbacks of enums:

  • Not a standard JS feature — generates runtime code
  • Numeric enums allow invalid values (Direction[99] is valid)
  • Can cause issues with tree-shaking
  • const enum breaks with isolatedModules

Alternative: as const object:

const Status = { Active: "ACTIVE", Inactive: "INACTIVE" } as const;
type Status = typeof Status[keyof typeof Status];  // "ACTIVE" | "INACTIVE"

Interview tricky questions

46. What does TypeScript do with void return type in callbacks?

type Fn = () => void;

// A function returning void CAN actually return a value
const arr: Fn[] = [];
arr.push(() => true);   // OK — return value ignored
arr.push(() => 42);     // OK

// But a function explicitly declared to return void cannot
function execute(): void {
  return true;  // Error
}

// This allows using Array.prototype.forEach's callback type flexibility
[1, 2, 3].forEach((n) => n * 2);  // OK even though forEach expects () => void

47. What is the difference between interface and type for functions?

// interface for function
interface Adder {
  (a: number, b: number): number;
  description: string;  // also has a property
}

// type for function
type Adder = (a: number, b: number) => number;

// Only interface can have overloads
interface Format {
  (value: string): string;
  (value: number, decimals: number): string;
}

48. How does TypeScript handle excess property checks?

TypeScript performs excess property checking when assigning object literals directly (not when assigning via a variable).

interface User { name: string; age: number; }

// Direct assignment — excess property check fires
const u: User = { name: "Alice", age: 30, email: "a@b.com" };  // Error — email

// Indirect — no check
const obj = { name: "Alice", age: 30, email: "a@b.com" };
const u: User = obj;  // OK — structural compatibility check only

// Workaround: index signature
interface Flexible { name: string; [key: string]: unknown; }
const f: Flexible = { name: "Alice", email: "a@b.com" };  // OK

49. What is ReturnType, Parameters, and InstanceType?

function createUser(name: string, age: number) {
  return { id: Math.random(), name, age, createdAt: new Date() };
}

type User = ReturnType<typeof createUser>;
// { id: number; name: string; age: number; createdAt: Date }

type CreateUserArgs = Parameters<typeof createUser>;
// [name: string, age: number]

class MyService {
  constructor(private db: Database) {}
  getUser(id: number) { return this.db.find(id); }
}

type ServiceInstance = InstanceType<typeof MyService>;
// MyService — equivalent to the class instance type

50. When should you use unknown vs generics?

// unknown — when you genuinely don't know the type yet, require narrowing
function parseJSON(json: string): unknown {
  return JSON.parse(json);
}
const data = parseJSON('{"name":"Alice"}');
// Must narrow before use

// Generic — when caller knows the type
function parseTyped<T>(json: string): T {
  return JSON.parse(json) as T;  // caller is responsible for correctness
}
const user = parseTyped<User>('{"name":"Alice","id":1}');
user.name;  // typed — no narrowing needed

// Best approach: zod / validation library
import { z } from "zod";
const UserSchema = z.object({ name: z.string(), id: z.number() });
const user = UserSchema.parse(JSON.parse(json));  // validated at runtime

Common mistakes table

Mistake Problem Fix
Using any everywhere Disables type safety Use unknown, generics, or proper types
Numeric enum Direction[99] is valid String enum or as const object
Ignoring strictNullChecks Null crashes at runtime Enable strict: true
Overusing type assertions Bypasses type system Use type guards / narrowing
interface for unions type A = B | C is impossible with interface Use type for unions
Missing return in type guard No narrowing Ensure predicate function has x is T return type
Readonly<T> is shallow Nested objects still mutable Use deep readonly or as const
const enum with bundlers Breaks with isolatedModules Use regular enum or as const

TypeScript vs JavaScript comparison

Feature JavaScript TypeScript
Type checking Runtime only Compile time
Null safety No Yes (strictNullChecks)
Interfaces No Yes
Generics No Yes
Enums No Yes (with caveats)
Decorators Stage 3 Stable (legacy or new)
IDE support Basic Full intellisense
Build step No Required

FAQ

Q: Should I use TypeScript for all projects?
TypeScript shines on medium-to-large projects and teams. For a quick personal script, plain JavaScript is fine. The break-even is usually when you have more than ~3 files or more than one contributor.

Q: What is isolatedModules and why does Vite/esbuild need it?
Bundlers that transpile files individually (without full type info) need isolatedModules: true. It disables const enum, type-only re-exports without type keyword, and other features requiring cross-file analysis.

Q: How do I type a dictionary / hash map?
Use Record<string, V> for simple key-value maps, or index signatures { [key: string]: V } for extensible interfaces. For exhaustive key sets use Record<UnionType, V>.

Q: What is the difference between T | undefined and T? in interfaces?
In an interface, prop?: T means the property may be absent or undefined. prop: T | undefined means the property must be present but its value may be undefined. With exactOptionalPropertyTypes: true these are enforced differently.

Q: How do I add types to a JavaScript library that lacks them?
Install @types/<package-name> from DefinitelyTyped. If no types exist, create a types/<package>.d.ts file with declare module "<package>" { ... }.

Q: What is the best way to share types between frontend and backend?
Extract shared types into a dedicated package (@myapp/types) or a shared directory imported by both. If using tRPC or Zod, derive types from schemas to have a single source of truth.

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools