Episode 1 — Fundamentals / 1.25 — TypeScript Essentials

Interview Questions: TypeScript Essentials

Model answers for TypeScript fundamentals, type system, generics, configuration, tooling, and best practices.

How to use this material (instructions)

  1. Read lessons in orderREADME.md, then 1.25.a through 1.25.l.
  2. Practice out loud — definition, example, pitfall.
  3. Pair with exercises1.25-Exercise-Questions.md.
  4. Quick review1.25-Quick-Revision.md.

Beginner (Q1–Q8)

Q1. What is TypeScript and how does it relate to JavaScript?

Why interviewers ask: Confirms you understand the foundational concept and the superset relationship.

Model answer:

TypeScript is a statically typed superset of JavaScript developed by Microsoft. "Superset" means every valid JavaScript program is also valid TypeScript — TypeScript only adds syntax (type annotations, interfaces, generics, enums). The TypeScript compiler (tsc) strips all type annotations and produces plain JavaScript that runs in any browser or Node.js runtime. Types exist at compile time only — there is zero runtime overhead. The key value is catching bugs before deployment, powering IDE intelligence (autocomplete, refactoring), and serving as living documentation.


Q2. What is the difference between any and unknown?

Why interviewers ask: Tests understanding of type safety and when to opt out.

Model answer:

Both accept any value, but they differ in how you can use that value:

  • any — disables all type checking. You can assign anything to it and use it as any type without checks. TypeScript trusts you blindly, which defeats the purpose of the type system.
  • unknown — the type-safe counterpart. You can assign anything to it, but you must narrow the type (using typeof, instanceof, or custom guards) before using it.
let a: any = "hello";
a.foo();                    // No error — but crashes at runtime

let b: unknown = "hello";
// b.foo();                 // Error: 'b' is of type 'unknown'
if (typeof b === "string") {
  b.toUpperCase();          // OK after narrowing
}

Best practice: Always prefer unknown over any. Use any only during migration from JavaScript.


Q3. Explain type inference in TypeScript.

Why interviewers ask: Shows you understand when annotations are needed versus redundant.

Model answer:

TypeScript automatically determines types from context without explicit annotations. When you write let x = 5, TypeScript infers x is number. When you write const arr = [1, 2, 3], it infers number[]. Function return types are also inferred from the return statement.

When inference works: Variables with initializers, function return types, array literals, object literals.

When you must annotate: Function parameters (always), empty arrays (let items: string[] = []), uninitialized variables (let name: string), and complex types where clarity matters.

Rule of thumb: Let inference work for local variables and internal code. Annotate public APIs (function parameters, return types of exported functions) for documentation and safety.


Q4. What is the difference between interface and type?

Why interviewers ask: One of the most common TypeScript questions. Tests nuanced understanding.

Model answer:

Both can define object shapes:

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

Key differences:

Featureinterfacetype
Object shapesYesYes
Extendsextends keyword& intersection
Declaration mergingYesNo
implements (classes)YesNo
Union typesNoYes (string | number)
Tuple typesNoYes ([string, number])
Primitive aliasesNoYes (type ID = string)
Mapped/conditional typesNoYes

Practical guideline: Use interface for object shapes (especially public APIs — they are extendable). Use type for unions, tuples, function types, and complex compositions. Consistency within a project matters most.


Q5. What is a union type and how do you narrow it?

Why interviewers ask: Unions and narrowing are core to TypeScript's type system.

Model answer:

A union type (A | B) means a value can be one of several types. You can only use properties/methods common to all types in the union — to use type-specific operations, you must narrow:

function process(input: string | number): string {
  if (typeof input === "string") {
    return input.toUpperCase();   // Narrowed to string
  } else {
    return input.toFixed(2);      // Narrowed to number
  }
}

Narrowing techniques: typeof (primitives), instanceof (classes), in operator (property check), equality checks (=== null), truthiness checks, discriminated unions (common literal property), and custom type guard functions (function isCat(x): x is Cat).


Q6. What is a tuple in TypeScript?

Why interviewers ask: Distinguishes TS-specific knowledge from plain JS.

Model answer:

A tuple is an array with a fixed number of elements where each position has a specific type:

type Coordinate = [number, number];
const point: Coordinate = [40.7128, -74.0060];

point[0].toFixed(2);  // OK — number
// point[2];           // Error — only 2 elements

Tuples are useful for function return values ([string, number]), destructuring patterns, and representing fixed-structure data. Named tuples ([lat: number, lng: number]) improve readability.

Unlike regular arrays (number[]), tuples enforce length and per-position types.


Q7. What does strict: true do in tsconfig.json?

Why interviewers ask: Shows you care about code quality and understand compiler configuration.

Model answer:

strict: true is a single flag that enables all strict type-checking options:

  • strictNullChecksnull/undefined must be handled explicitly
  • noImplicitAny — errors on implicit any (must annotate)
  • strictFunctionTypes — stricter function parameter checking
  • strictPropertyInitialization — class properties must be initialized
  • useUnknownInCatchVariablescatch(e) gives e: unknown instead of any
  • And several more

It is always recommended for new projects because it catches the most bugs. For migrating existing JS projects, you can enable individual strict flags one at a time.


Q8. What is the purpose of @types/* packages?

Why interviewers ask: Practical knowledge for working with the TypeScript ecosystem.

Model answer:

@types/* packages provide type definitions (.d.ts files) for JavaScript libraries that do not ship their own types. They come from the DefinitelyTyped community project.

npm install express           # The library (JavaScript)
npm install -D @types/express # Type definitions for Express

TypeScript automatically picks them up from node_modules/@types/. Libraries written in TypeScript (like zod, axios, prisma) already include types — you do not need @types/* for those. Check package.json for a "types" field to see if types are bundled.


Intermediate (Q9–Q16)

Q9. Explain generics and why they matter.

Why interviewers ask: Generics are central to writing reusable TypeScript code.

Model answer:

Generics let you write functions, interfaces, and classes that work with any type while preserving type safety. Without generics, you choose between any (reusable but unsafe) or writing separate functions for each type (safe but repetitive).

// Without generics:
function firstAny(arr: any[]): any { return arr[0]; }  // Unsafe

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

first([1, 2, 3]);       // number | undefined — type preserved!
first(["a", "b"]);      // string | undefined — type preserved!

TypeScript usually infers generic type arguments from the values you pass, so you rarely specify them explicitly. Generics are used extensively in TypeScript's standard library: Array<T>, Promise<T>, Map<K, V>, Record<K, V>.


Q10. What is a discriminated union and when would you use it?

Why interviewers ask: Tests understanding of TypeScript's most powerful pattern.

Model answer:

A discriminated union is a union of types that share a common literal property (the "discriminant") used to distinguish between variants:

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;
  }
}

When to use: API responses with different structures, state machines (loading/success/error), Redux actions, event handling — any scenario where data can take multiple forms and you need to safely handle each one.

Bonus: Adding default: const _: never = shape provides exhaustiveness checking — the compiler errors if you forget a case.


Q11. Explain Partial<T>, Pick<T, K>, and Omit<T, K>.

Why interviewers ask: Tests knowledge of built-in utility types — frequently used in real codebases.

Model answer:

  • Partial<T> — makes all properties of T optional. Useful for update functions where you only change some fields:

    function updateUser(id: number, updates: Partial<User>): User { /* ... */ }
    updateUser(1, { name: "New Name" });  // Only updating 'name'
    
  • Pick<T, K> — creates a type with only the listed properties:

    type PublicUser = Pick<User, "id" | "name" | "email">;
    // { id: number; name: string; email: string } — no password
    
  • Omit<T, K> — creates a type without the listed properties:

    type CreateInput = Omit<User, "id" | "createdAt">;
    // Everything except id and createdAt
    

Under the hood, these are implemented with mapped types and keyof.


Q12. What is the difference between tsc and bundlers like Vite/esbuild for TypeScript?

Why interviewers ask: Tests understanding of the modern TypeScript build pipeline.

Model answer:

tsc does two jobs: type checking and JavaScript emission. Bundlers like Vite/esbuild do only one: they strip types to produce JavaScript, without any type analysis.

This means bundlers are much faster (esbuild is 10-100x faster than tsc) but will not catch type errors. A TypeScript file with incorrect types will compile to JavaScript that runs — and may crash at runtime.

Modern workflow: Use the bundler for fast compilation and tsc --noEmit separately for type checking:

tsc --noEmit && vite build
# ↑ type-check    ↑ compile and bundle

Q13. How does TypeScript handle null and undefined?

Why interviewers ask: Null-related bugs are the most common category of runtime errors.

Model answer:

With strictNullChecks: true (included in strict: true), null and undefined are separate types that cannot be assigned to other types:

let name: string = null;  // Error with strictNullChecks

let maybeName: string | null = null;  // OK — explicitly nullable

You must narrow before using nullable values:

function greet(name: string | null): string {
  if (name === null) return "Hello, stranger!";
  return `Hello, ${name.toUpperCase()}!`;  // Safe — narrowed to string
}

TypeScript also provides: optional chaining (user?.name), nullish coalescing (value ?? default), and optional properties (name?: string — property may be absent).

Always enable strictNullChecks — it eliminates the most common category of runtime bugs.


Q14. What are mapped types?

Why interviewers ask: Shows deeper understanding of the type system.

Model answer:

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

type Readonly<T> = { readonly [K in keyof T]: T[K] };
type Optional<T> = { [K in keyof T]?: T[K] };
type Stringify<T> = { [K in keyof T]: string };

The syntax [K in keyof T] iterates over every key K in type T. You can modify the value type (T[K] to string), add/remove modifiers (readonly, ?), or apply conditional types.

TypeScript's built-in utility types (Partial, Required, Readonly, Pick, Record) are all implemented as mapped types.


Q15. Explain the keyof operator.

Why interviewers ask: keyof is fundamental to advanced TypeScript patterns.

Model answer:

keyof produces a union type of all property names (keys) of a type:

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

It is commonly used with generics for 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" };
getProperty(user, "name");    // string
getProperty(user, "id");      // number
// getProperty(user, "foo");  // Error: "foo" not in keyof User

Q16. How would you set up ESLint with TypeScript?

Why interviewers ask: Practical project setup knowledge.

Model answer:

Install @typescript-eslint/parser (teaches ESLint to parse TS syntax) and @typescript-eslint/eslint-plugin (provides TS-specific rules). Use eslint-config-prettier if also using Prettier to avoid conflicts.

Key TypeScript ESLint rules: no-explicit-any (ban any), no-unused-vars (catch dead code), no-floating-promises (unhandled promises), consistent-type-imports (use import type).

ESLint catches code quality issues that the type system misses — patterns, unused code, potential bugs. TypeScript catches type errors. They are complementary, not redundant.


Advanced (Q17–Q24)

Q17. Explain conditional types and infer.

Why interviewers ask: Tests advanced type system knowledge.

Model answer:

Conditional types follow the pattern T extends U ? X : Y — "if T assignable to U, use X; otherwise Y":

type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">;  // true
type B = IsString<42>;       // false

The infer keyword extracts a type within the condition:

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

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

Real use cases: ReturnType<T>, Parameters<T>, Awaited<T> (unwraps Promises) — all built-in utility types.


Q18. What is declaration merging and when is it useful in practice?

Why interviewers ask: Tests knowledge of TypeScript's module system and augmentation.

Model answer:

Declaration merging lets multiple interface declarations with the same name combine into a single interface:

interface Window { analytics: { track(event: string): void } }

This adds analytics to the existing Window interface without modifying the lib.dom.d.ts source.

Practical uses:

  1. Augmenting Express Request — adding userId, session to request objects
  2. Extending Window — adding global analytics, feature flags
  3. Module augmentation — adding types to third-party libraries

Type aliases cannot merge — this is one of the key practical differences between interface and type.


Q19. How do you handle TypeScript in a monorepo?

Why interviewers ask: Tests real-world experience with larger projects.

Model answer:

Use project references with tsc --build:

  1. Each package has its own tsconfig.json with "composite": true
  2. Dependent packages reference others via "references": [{ "path": "../shared" }]
  3. tsc -b builds everything in dependency order, caching results

Benefits: incremental builds (only recompile what changed), clear dependency graph, faster CI.

Additional strategies: shared tsconfig.base.json (common options via extends), path aliases for cross-package imports, @tsconfig/node20 or similar community configs for consistency.


Q20. Explain type guards and user-defined type guards.

Why interviewers ask: Type narrowing is essential for production TypeScript.

Model answer:

Built-in guards: typeof (primitives), instanceof (classes), in (property existence), equality checks (===).

User-defined type guards use a type predicate return type (param is Type):

interface Cat { meow(): void; }
interface Dog { bark(): void; }

function isCat(animal: Cat | Dog): animal is Cat {
  return "meow" in animal;
}

function handle(animal: Cat | Dog) {
  if (isCat(animal)) {
    animal.meow();   // TypeScript knows: Cat
  } else {
    animal.bark();   // TypeScript knows: Dog
  }
}

Custom guards are needed when built-in guards are insufficient — for example, validating API response shapes, distinguishing between custom interfaces, or writing assertion functions.


Q21. What are template literal types?

Why interviewers ask: Tests knowledge of advanced type features.

Model answer:

Template literal types use backtick syntax at the type level to create string patterns:

type EventName = `${"user" | "order"}:${"created" | "updated" | "deleted"}`;
// "user:created" | "user:updated" | "user:deleted" |
// "order:created" | "order:updated" | "order:deleted"

type HttpEndpoint = `/${string}`;
type CssProperty = `--${string}`;

Useful for typed event systems, CSS-in-JS, API route definitions, and configuration keys. Combined with infer, they can also parse string types.


Q22. Explain variance in TypeScript (covariance and contravariance).

Why interviewers ask: Deep type system knowledge relevant to function types and generics.

Model answer:

Covariance (output positions): If Dog extends Animal, then Producer<Dog> is assignable to Producer<Animal>. Array element types are covariant: Dog[] is assignable to Animal[].

Contravariance (input positions): Function parameters are contravariant with strictFunctionTypes. If Dog extends Animal, a function accepting Animal is assignable to one accepting Dog (not the other way).

type Getter<T> = () => T;          // Covariant in T
type Setter<T> = (value: T) => void; // Contravariant in T

This matters when designing generic interfaces and callback types — incorrect variance can create unsound type holes.


Q23. How do you type a higher-order function in TypeScript?

Why interviewers ask: Tests ability to handle complex function types.

Model answer:

// Function that returns a function
function createMultiplier(factor: number): (n: number) => number {
  return (n) => n * factor;
}

// Generic higher-order function
function pipe<A, B, C>(
  f: (a: A) => B,
  g: (b: B) => C
): (a: A) => C {
  return (a) => g(f(a));
}

const transform = pipe(
  (n: number) => n.toString(),
  (s: string) => s.length
);
transform(42);  // 2 — number → string → number

Key techniques: explicit function type annotations, generic type parameters for preserving types through the chain, and Parameters<T> / ReturnType<T> for extracting types from existing functions.


Q24. What is the satisfies operator and when would you use it?

Why interviewers ask: Relatively new feature (TS 4.9) — tests staying current.

Model answer:

satisfies checks that a value matches a type without widening the inferred type:

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

// With 'as Colors' — loses specific types:
const palette = { red: [255, 0, 0], green: "#00ff00" } as Colors;
palette.red;  // string | number[] — lost the specific array type

// With 'satisfies' — keeps specific types:
const palette = { red: [255, 0, 0], green: "#00ff00" } satisfies Colors;
palette.red;    // number[] — preserved!
palette.green;  // string — preserved!

Use satisfies when you want to validate that a value conforms to a type while keeping the narrower inferred type for downstream usage. Common for configuration objects, color palettes, route maps.


Quick-fire

#QuestionOne-line answer
1TypeScript compiles to?JavaScript — types are stripped
2string | number is a?Union type
3A & B is a?Intersection type
4Partial<T> does?Makes all properties optional
5keyof T returns?Union of property names of T
6tsc --noEmit does?Type-checks only — no output
7strict: true means?All strict type checks enabled
8.d.ts files are?Type declarations (no implementation)
9as const does?Makes values readonly and literal
10never type means?Value that never occurs

← Back to 1.25 — TypeScript Essentials (README)