Skip to main content

Module 34: Interview Preparation

Master TypeScript interview questions, coding challenges, and common patterns asked by top companies.


1. Type System Questions

Q1: What is the difference between type and interface?

// Interface - can be extended and merged
interface User {
name: string;
}

interface User { // Declaration merging
age: number;
}

interface Admin extends User { // Can extend
role: string;
}

// Type - more flexible, can't be merged
type User = {
name: string;
};

type Admin = User & { // Must use intersection
role: string;
};

// Type can represent unions, primitives, tuples
type ID = string | number;
type Point = [number, number];

Answer: Interfaces can be extended and support declaration merging. Types are more flexible and can represent unions, primitives, and complex types.


Q2: Explain unknown vs any

// any - disables type checking
let value: any = "hello";
value.toUpperCase(); // OK
value.nonExistent(); // OK (no error)

// unknown - requires type checking
let value2: unknown = "hello";
value2.toUpperCase(); // Error
if (typeof value2 === "string") {
value2.toUpperCase(); // OK
}

Answer: unknown is type-safe, requiring type guards before use. any disables type checking completely.


Q3: What are conditional types?

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

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

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

2. Advanced Type Challenges

Challenge 1: Deep Readonly

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

interface Data {
user: {
name: string;
address: {
street: string;
};
};
}

type ReadonlyData = DeepReadonly<Data>;
// All nested properties are readonly

Challenge 2: Pick By Type

type PickByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
};

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

type StringProps = PickByType<User, string>;
// { name: string; email: string; }

Challenge 3: Required Keys

type RequiredKeys<T> = {
[K in keyof T]-?: {} extends Pick<T, K> ? never : K;
}[keyof T];

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

type Required = RequiredKeys<User>; // "name" | "email"

Challenge 4: Function Property Names

type FunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];

interface User {
name: string;
getName(): string;
age: number;
setAge(age: number): void;
}

type Methods = FunctionPropertyNames<User>;
// "getName" | "setAge"

3. Practical Coding Problems

Problem 1: Type-Safe Event Emitter

type EventMap = {
"user:created": { id: string; name: string };
"user:deleted": { id: string };
"post:published": { postId: string; authorId: string };
};

class TypedEventEmitter<T extends Record<string, any>> {
private listeners = new Map<keyof T, Set<Function>>();

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

emit<K extends keyof T>(event: K, data: T[K]): void {
const callbacks = this.listeners.get(event);
if (callbacks) {
callbacks.forEach(cb => cb(data));
}
}
}

const emitter = new TypedEventEmitter<EventMap>();

emitter.on("user:created", (data) => {
// data is typed as { id: string; name: string }
console.log(data.name);
});

emitter.emit("user:created", { id: "1", name: "John" }); // OK
emitter.emit("user:created", { id: "1" }); // Error: missing name

Problem 2: Type-Safe Builder Pattern

class QueryBuilder<T extends Record<string, any>> {
private filters: Partial<T> = {};
private sortField?: keyof T;
private limitValue?: number;

where<K extends keyof T>(field: K, value: T[K]): this {
this.filters[field] = value;
return this;
}

sort(field: keyof T): this {
this.sortField = field;
return this;
}

limit(value: number): this {
this.limitValue = value;
return this;
}

build(): QueryParams<T> {
return {
filters: this.filters,
sort: this.sortField,
limit: this.limitValue
};
}
}

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

const query = new QueryBuilder<User>()
.where("name", "John") // Type-safe
.where("age", 25) // Type-safe
.sort("name") // Type-safe
.limit(10)
.build();

Problem 3: Type-Safe State Machine

type State = "idle" | "loading" | "success" | "error";

type Transitions = {
idle: "loading";
loading: "success" | "error";
success: "idle";
error: "idle";
};

class StateMachine<S extends string, T extends Record<S, string>> {
constructor(private state: S) {}

getState(): S {
return this.state;
}

transition<From extends S, To extends T[From]>(
from: From,
to: To extends S ? To : never
): void {
if (this.state !== from) {
throw new Error(`Invalid transition from ${this.state}`);
}
this.state = to as S;
}
}

const machine = new StateMachine<State, Transitions>("idle");

machine.transition("idle", "loading"); // OK
machine.transition("loading", "success"); // OK
machine.transition("idle", "success"); // Error: invalid transition

4. Common Interview Questions

Q4: Difference between never and void?

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

// never - function never returns
function throwError(message: string): never {
throw new Error(message);
// execution never reaches end
}

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

Q5: What is type narrowing?

function process(value: string | number) {
if (typeof value === "string") {
// TypeScript narrows type to string
console.log(value.toUpperCase());
} else {
// TypeScript narrows type to number
console.log(value.toFixed(2));
}
}

Q6: Explain generic constraints

// Without constraint
function getProperty<T, K>(obj: T, key: K) {
return obj[key]; // Error: K might not be in T
}

// With constraint
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]; // OK: K is guaranteed to be in T
}

const user = { name: "John", age: 25 };
getProperty(user, "name"); // OK
getProperty(user, "invalid"); // Error

Q7: What is mapped type?

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

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

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

type ReadonlyUser = Readonly<User>;
// { readonly name: string; readonly age: number; }

type PartialUser = Optional<User>;
// { name?: string; age?: number; }

Q8: Difference between Partial and Required?

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

type PartialUser = Partial<User>;
// { name?: string; age?: number; email?: string; }

type RequiredUser = Required<User>;
// { name: string; age: number; email: string; }

5. Performance Questions

Q9: How to optimize TypeScript compilation?

{
"compilerOptions": {
"incremental": true, // Enable incremental compilation
"skipLibCheck": true, // Skip type checking .d.ts files
"skipDefaultLibCheck": true // Skip default lib checking
},
"exclude": ["node_modules", "dist"]
}

Q10: What are declaration files?

// math.d.ts - type declarations only
export function add(a: number, b: number): number;
export function subtract(a: number, b: number): number;

// Usage
import { add } from "./math";
const result = add(2, 3); // Type-safe without implementation

6. Behavioral Questions

Q11: When would you use any vs unknown?

  • Use unknown when the type is truly unknown and will be validated
  • Avoid any unless absolutely necessary for gradual migration

Q12: How do you handle type-safety with third-party libraries?

  • Install @types packages
  • Create custom .d.ts files
  • Use module augmentation

Q13: What's your approach to error handling in TypeScript?

  • Use custom error classes
  • Implement Result/Either types
  • Type-safe error boundaries

Key Takeaways

Type vs Interface - know when to use each
Unknown vs Any - prefer unknown for safety
Conditional types for advanced patterns
Type guards for narrowing
Generic constraints for type safety
✅ Practice type challenges regularly


Practice Resources


Practice Exercises

Exercise 1: Implement DeepPartial

Create a type that makes all nested properties optional.

Exercise 2: Type-Safe API Client

Build a type-safe REST API client with proper typing.

Exercise 3: Advanced Utility Types

Implement custom utility types like DeepReadonly, PickByType, RequiredKeys.


Next Steps

In Module 35, we'll explore TypeScript Future and Best Practices to complete your TypeScript mastery journey.