TypeScriptInterview Questions

TypeScript Interview Questions

This page covers 25 TypeScript interview questions ranging from fundamentals to advanced type-system topics. Each question includes a thorough answer and code examples. Use these to prepare for interviews or to check the depth of your own understanding.

Beginner Questions

1. What is TypeScript and why use it over plain JavaScript?

TypeScript is a statically-typed superset of JavaScript developed by Microsoft. It compiles to plain JavaScript and runs anywhere JavaScript runs. Key benefits:

  • Compile-time error detection — typos, wrong argument types, and missing properties are caught before the code runs
  • Autocompletion and navigation — editors can provide accurate IntelliSense because types are known
  • Self-documenting code — function signatures describe what they accept and return
  • Safer refactoring — rename a property and the compiler shows every place that breaks

TypeScript adds zero runtime overhead — all type annotations are erased during compilation.

2. What is the difference between interface and type?

Both describe object shapes, but they differ in key ways:

TS
// Interfaces can be extended and merged
interface Animal { name: string; }
interface Dog extends Animal { breed: string; }

// Declaration merging — only interfaces support this
interface Window { myCustomProp: string; }
interface Window { anotherProp: number; }

// Types support union, intersection, and computed types that interfaces cannot
type StringOrNumber = string | number;
type Keys = keyof { a: 1; b: 2 };          // "a" | "b"
type Pair<T> = [T, T];                       // tuple alias

// Intersection — both work, but syntax differs
type AB = { a: number } & { b: string };    // type
interface IAB extends IA, IB {}              // interface
Tip
Prefer interface for object shapes that may be extended or augmented (e.g. libraries, class contracts). Use type for unions, intersections, mapped types, and aliases for primitives.

3. What is any and why should you avoid it?

any disables type checking for a value — it can be assigned to and from any type without error. This defeats the purpose of TypeScript:

TS
let x: any = 42;
x.toUpperCase();  // No error! But crashes at runtime: x.toUpperCase is not a function
x = 'hello';
x = { foo: 'bar' };

// Better alternatives:
// unknown — forces you to narrow before using
let y: unknown = fetchData();
if (typeof y === 'string') y.toUpperCase();  // Safe

// Specific union
let z: string | number | null = getResult();

4. What is the difference between unknown and any?

Both accept any value, but unknown is the type-safe alternative to any. You cannot perform operations on unknown without first narrowing it to a specific type:

TS
let a: any     = "hello";
let b: unknown = "hello";

a.toUpperCase();   // OK (but unsafe)
b.toUpperCase();   // Error: Object is of type 'unknown'

// Must narrow first
if (typeof b === 'string') {
  b.toUpperCase(); // OK — narrowed to string
}

5. What are union types and intersection types?

A union type (A | B) represents a value that can be either A or B. An intersection type (A & B) represents a value that is both A and B simultaneously:

TS
// Union — one or the other
type ID = string | number;
function printId(id: ID) {
  if (typeof id === 'string') console.log(id.toUpperCase());
  else console.log(id.toFixed(0));
}

// Intersection — combines both shapes
interface HasName  { name: string; }
interface HasEmail { email: string; }

type Contact = HasName & HasEmail;
// { name: string; email: string }

const c: Contact = { name: 'Alice', email: 'a@b.com' }; // must have both
Intermediate Questions

6. What is type narrowing? List the common narrowing guards.

Narrowing is the process by which TypeScript refines a broader type to a more specific one within a conditional block:

TS
function process(value: string | number | null | { tag: 'circle'; radius: number }) {
  // typeof guard
  if (typeof value === 'string') {
    return value.toUpperCase(); // string
  }

  // null / falsy check
  if (value === null) {
    return 'nothing';
  }

  // typeof guard (number)
  if (typeof value === 'number') {
    return value.toFixed(2);
  }

  // Discriminant property guard
  if (value.tag === 'circle') {
    return Math.PI * value.radius ** 2;
  }
}

// instanceof guard
function formatDate(value: Date | string) {
  if (value instanceof Date) return value.toISOString();
  return new Date(value).toISOString();
}

// in guard
function hasOwn<T extends object>(obj: T, key: string): key is keyof T {
  return key in obj;
}

7. What are generics and when should you use them?

Generics let you write a single function or class that works with any type while preserving type relationships. Use them when the return type depends on the input type:

TS
// Without generics — loses the relationship between input and output
function identity(x: any): any { return x; }

// With generics — output type is the same as input type
function identity<T>(x: T): T { return x; }

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

// Generic constraint — 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("abc", "de");              // OK
longest([1, 2, 3], [4, 5]);        // OK
longest(10, 20);                    // Error: number has no length

8. What is a discriminated union and why is it useful?

A discriminated union is a union of object types that each have a common literal property (the "discriminant") with a unique value. It enables exhaustive pattern matching:

TS
type Shape =
  | { kind: 'circle';   radius: number }
  | { kind: 'square';   side: number }
  | { kind: 'triangle'; base: 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 'triangle': return (shape.base * shape.height) / 2;
    // TypeScript ensures all cases are handled — add 'default: assertNever(shape)'
  }
}

9. What is the keyof operator?

keyof T produces a union of all property names (keys) of type T as string literal types:

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

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

// Practical use: type-safe property access
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user: User = { id: 1, name: 'Alice', email: 'a@b.com' };

const name  = getProperty(user, 'name');   // string
const id    = getProperty(user, 'id');     // number
const email = getProperty(user, 'email'); // string

getProperty(user, 'age');  // Error: 'age' is not a key of User

10. What are mapped types?

Mapped types iterate over the keys of a type and produce a new type with transformed properties:

TS
// Make every property optional
type Partial<T> = {
  [K in keyof T]?: T[K];
};

// Make every property readonly
type Readonly<T> = {
  readonly [K in keyof T]: T[K];
};

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

// Remove a modifier with -? or -readonly
type Required<T> = {
  [K in keyof T]-?: T[K];  // -? removes optional
};

11. What are conditional types?

Conditional types work like ternary operators at the type level: T extends U ? A : B. If T is assignable to U, the result is A; otherwise B:

TS
type IsArray<T> = T extends unknown[] ? true : false;

type A = IsArray<string[]>;   // true
type B = IsArray<string>;     // false

// With infer — extract sub-types
type ElementType<T> = T extends (infer E)[] ? E : never;

type E1 = ElementType<string[]>;   // string
type E2 = ElementType<number[][]>; // number[]

// Distribution over unions
type Flatten<T> = T extends (infer I)[] ? I : T;
type F = Flatten<string[] | number | boolean[]>;  // string | number | boolean
Advanced Questions

12. Explain the infer keyword with examples.

infer appears inside the extends clause of a conditional type and captures a type into a new variable R. It is TypeScript's pattern-matching mechanism for types:

TS
// Built-in utility types implemented with infer
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type Parameters<T> = T extends (...args: infer P) => any  ? P : never;
type InstanceType<T> = T extends new (...args: any[]) => infer I ? I : never;

// Custom: extract the value type from a Promise chain
type Awaited<T> = T extends Promise<infer R> ? Awaited<R> : T;
type Deep = Awaited<Promise<Promise<Promise<string>>>>;  // string

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

13. What is declaration merging?

Declaration merging combines multiple declarations with the same name into a single definition. Only interface and namespace declarations can merge:

TS
// Interface merging — useful for augmenting third-party types
interface Window { myAppVersion: string; }
interface Window { myAnalytics: (event: string) => void; }
// Result: Window has both properties

// Module augmentation — add properties to third-party types
declare module 'express' {
  interface Request {
    user?: { id: string; role: string };
  }
}

// Namespace + interface merging (function with properties)
function parse(input: string): ParseResult { /* ... */ }
namespace parse {
  export interface Options { strict: boolean; }
  export function fromJSON(json: string): ParseResult { /* ... */ }
}

parse("hello");                       // function call
parse.fromJSON('{"x":1}');            // namespace method
const opts: parse.Options = { strict: true };  // namespace type

14. What is the difference between structural and nominal typing?

TypeScript uses structural typing: compatibility is determined by the shape of types, not their names. Two types with the same properties are interchangeable, even if declared separately:

TS
interface Point2D { x: number; y: number; }
interface Vector   { x: number; y: number; }

function move(p: Point2D) { /* ... */ }

const v: Vector = { x: 1, y: 2 };
move(v);  // OK — same shape, even different type name

// Nominal typing must be simulated with brands
type UserId  = string & { _brand: 'UserId' };
type OrderId = string & { _brand: 'OrderId' };

function getUser(id: UserId) { /* ... */ }

const oid = "o-1" as unknown as OrderId;
getUser(oid);  // Error — OrderId is not UserId

15. What is covariance and contravariance in TypeScript?

Covariance means the subtype relationship is preserved: if Dog extends Animal, then () => Dog is a subtype of () => Animal (return types are covariant). Contravariance means the relationship is reversed: (animal: Animal) => void is a subtype of (dog: Dog) => void (parameter types are contravariant with --strictFunctionTypes):

TS
class Animal { name = ''; }
class Dog extends Animal { bark() {} }

// Covariant return type — Dog producer satisfies Animal producer
type GetAnimal = () => Animal;
type GetDog    = () => Dog;
const getDog: GetDog = () => new Dog();
const getAnimal: GetAnimal = getDog;  // OK

// Contravariant parameter — Animal handler satisfies Dog handler
type HandleAnimal = (a: Animal) => void;
type HandleDog    = (d: Dog)    => void;
const handleAnimal: HandleAnimal = (a) => a.name;
const handleDog: HandleDog = handleAnimal;  // OK — more permissive handler is safe

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

satisfies validates that a value matches a type without widening the inferred type. It gives you the strictness of type checking while preserving the specific literal types:

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

// Without satisfies — color is Record<string, ...> and loses specific member types
const palette: Colors = {
  red: [255, 0, 0],
  green: '#00ff00',
};
palette.red.map;   // Error — TypeScript only knows it's array | string

// With satisfies — validates the shape AND keeps specific types
const palette2 = {
  red:   [255, 0, 0],
  green: '#00ff00',
} satisfies Colors;

palette2.red.map;    // OK — TypeScript knows this member is an array
palette2.green.toUpperCase();  // OK — TypeScript knows this is a string

17. How do template literal types work?

Template literal types construct new string literal types by combining other types, mirroring JavaScript template literals:

TS
type EventName<T extends string> = `on${Capitalize<T>}`;
type Getters<T extends string>   = `get${Capitalize<T>}`;

type MouseEvent  = EventName<'click' | 'hover'>;  // 'onClick' | 'onHover'
type UserGetters = Getters<'name' | 'email'>;      // 'getName' | 'getEmail'

// Extract method name from a CSS-in-JS object
type CSSVar<T extends string> = `--${T}`;
type Colors = CSSVar<'primary' | 'secondary' | 'accent'>;
// '--primary' | '--secondary' | '--accent'

// infer with template literals
type StripPrefix<T extends string, Prefix extends string> =
  T extends `${Prefix}${infer Rest}` ? Rest : T;

type Name = StripPrefix<'onFocus', 'on'>;  // 'Focus'

18. What is exhaustiveness checking and how do you implement it?

Exhaustiveness checking ensures that a switch or if-else chain handles every possible case of a union type. You implement it with the never type via an assertNever function:

TS
function assertNever(value: never): never {
  throw new Error(`Unexpected value: ${JSON.stringify(value)}`);
}

type Shape =
  | { kind: 'circle';   radius: number }
  | { kind: 'square';   side: number   };

function area(shape: Shape): number {
  switch (shape.kind) {
    case 'circle': return Math.PI * shape.radius ** 2;
    case 'square': return shape.side ** 2;
    default:       return assertNever(shape);
    // If you add a new Shape variant without handling it, TypeScript errors HERE
  }
}

// Now add a new shape — TypeScript immediately shows the error in assertNever
type Shape2 =
  | { kind: 'circle';   radius: number }
  | { kind: 'square';   side: number   }
  | { kind: 'triangle'; base: number; height: number };  // NEW

19. What are decorators and how are they typed?

Decorators are special functions that modify classes, methods, properties, or parameters. They are available with experimentalDecorators: true (legacy) or natively in TypeScript 5+:

TS
// TypeScript 5+ native decorator (no experimentalDecorators needed)
function log<T, C extends abstract new (...args: any[]) => T>(
  target: C,
  context: ClassDecoratorContext<C>
) {
  console.log(`Class ${context.name} was defined`);
  return target;
}

@log
class UserService {
  findAll() { /* ... */ }
}

// Method decorator
function readonly(target: unknown, context: ClassMethodDecoratorContext) {
  context.addInitializer(function (this: unknown) {
    Object.defineProperty(this, context.name, { writable: false });
  });
}

class Config {
  @readonly
  getSecret() { return 'secret'; }
}

20. What is module augmentation?

Module augmentation extends a module's exported types from a separate file. This is how you add properties to third-party types (like Express's Request) without forking the package:

TS
// In your types file (e.g. src/types/express.d.ts)
// The import makes this a module (not a global script)
import {} from 'express';

declare module 'express-serve-static-core' {
  interface Request {
    user?: { id: string; role: 'admin' | 'user' };
    requestId: string;
  }
}

// Now req.user and req.requestId are typed everywhere in the project
app.get('/me', authenticate, (req, res) => {
  res.json(req.user);        // { id: string; role: 'admin' | 'user' } | undefined
  console.log(req.requestId); // string
});

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

as const makes an entire value deeply immutable and infers the narrowest possible literal types. readonly applies only to properties/array elements at the type level:

TS
// readonly — one level deep, still mutable nested properties
const arr: readonly string[] = ['a', 'b', 'c'];
arr.push('d');   // Error
arr[0] = 'x';   // Error

// as const — deep, infers literal types
const config = {
  host: 'localhost',
  port: 3000,
  tags: ['api', 'v2'],
} as const;

// config.host is 'localhost' (literal), not string
// config.port is 3000 (literal), not number
// config.tags is readonly ['api', 'v2'] (tuple), not string[]

config.host = 'remote';  // Error — deeply readonly

22. How do you type a function with overloads?

Function overloads let you declare multiple signatures for a single function, then provide one implementation that handles all of them:

TS
// Overload signatures — visible to callers
function parse(input: string):  Date;
function parse(input: number):  Date;
function parse(input: Date):    Date;

// Implementation signature — must be compatible with all overloads
function parse(input: string | number | Date): Date {
  if (input instanceof Date) return input;
  if (typeof input === 'number') return new Date(input);
  return new Date(input);
}

const d1 = parse("2024-01-01");  // Date
const d2 = parse(1700000000000); // Date
const d3 = parse(new Date());    // Date

23. What is a type predicate and when do you use it?

A type predicate is a function that narrows a type and its return type is written as paramName is Type. Use it when a narrowing condition is too complex for TypeScript to infer automatically:

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

// Type predicate — tells TypeScript "if this returns true, pet is Fish"
function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

function move(pet: Fish | Bird) {
  if (isFish(pet)) {
    pet.swim();  // TypeScript knows pet is Fish here
  } else {
    pet.fly();   // TypeScript knows pet is Bird here
  }
}

// Array filter with type predicate
const values: (string | null | undefined)[] = ['a', null, 'b', undefined, 'c'];

function isString(v: string | null | undefined): v is string {
  return typeof v === 'string';
}

const strings = values.filter(isString);  // string[] (not (string | null | undefined)[])

24. What is the never type and where does it appear?

never is TypeScript's bottom type — it represents a value that can never exist. It appears in:

  • Functions that never return (they throw or run forever)
  • Exhausted union types in conditional branches
  • The return type of assertNever
  • Impossible intersections (string & number = never)

TS
// Never-returning function
function fail(message: string): never {
  throw new Error(message);
}

function infiniteLoop(): never {
  while (true) {}
}

// Narrowed to never — all cases exhausted
function processStatus(status: 'active' | 'inactive') {
  if (status === 'active') { /* ... */ }
  else if (status === 'inactive') { /* ... */ }
  else {
    // status is never here — all cases handled
    const _exhaustive: never = status;
  }
}

// Impossible intersection
type Impossible = string & number;  // never

25. How do you build a deep-readonly type recursively?

This is a common type-level programming challenge that tests knowledge of mapped types, conditional types, and recursion:

TS
type DeepReadonly<T> =
  T extends (infer E)[]
    ? ReadonlyArray<DeepReadonly<E>>
    : T extends object
      ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
      : T;

interface Config {
  server: { host: string; port: number; };
  db:     { url: string; replicas: string[]; };
}

type FrozenConfig = DeepReadonly<Config>;
/*
{
  readonly server: { readonly host: string; readonly port: number; };
  readonly db:     { readonly url: string; readonly replicas: ReadonlyArray<string>; };
}
*/

const cfg: FrozenConfig = {
  server: { host: 'localhost', port: 3000 },
  db:     { url: 'postgres://...', replicas: ['r1', 'r2'] },
};

cfg.server.host = 'remote';     // Error — deeply readonly
cfg.db.replicas.push('r3');     // Error — ReadonlyArray has no push
Quick-Reference Summary

Topic

Key Concept

any vs unknown

unknown requires narrowing before use; any skips all checks

interface vs type

interface merges/extends; type handles unions and computed types

Generics

Preserve type relationships across inputs and outputs

keyof / typeof

keyof extracts property names; typeof extracts the type of a value

Mapped types

Transform all properties of an object type with [K in keyof T]

Conditional types

T extends U ? A : B — if/else at the type level

infer

Extract sub-types from patterns inside conditional types

Discriminated unions

Common literal property enables exhaustive pattern matching

never

Bottom type — unreachable code, exhausted unions, impossible intersections

Narrowing

typeof, instanceof, in, literal checks refine a union to a specific member

satisfies

Validate shape without widening — keeps specific literal types

Variance

Covariant in return position; contravariant in parameter position

Branded types

Phantom property distinguishes structurally identical primitive types

Decorators

Compile-time annotations that wrap classes, methods, and properties

Module augmentation

Extend third-party types without forking the package