TypeScript — Tricky Interview Questions
Q1: What is the output type?
typescripttype A = string extends any ? 1 : 0;
type B = any extends string ? 1 : 0;Answer: A = 1, B = 0 | 1 (i.e., number)
Why: string extends any is always true (any is everything). any extends string is special — any produces a union of both branches: 1 | 0. TypeScript treats any as matching both sides of a conditional.
Q2: Structural Typing Surprise
typescriptinterface Empty {}
interface HasName { name: string; }
function greet(obj: Empty): void {}
greet({ name: 'Alice', age: 30 }); // ❌ or ✅?
greet({} as HasName); // ❌ or ✅?Answer: Both ✅ work!
Why: Structural typing — Empty has no required properties, so ANY object satisfies it. HasName is a superset of Empty (it has all required properties + more). Fresh object literals with extra properties trigger "excess property checks" but passing a variable or cast avoids that.
Q3: Function Parameter Variance
typescripttype Logger = (msg: string) => void;
const l1: Logger = (msg: string | number) => {}; // ✅ or ❌?
const l2: Logger = (msg: 'hello') => {}; // ✅ or ❌?Answer: l1 ✅, l2 ❌
Why: Function parameters are contravariant (with strictFunctionTypes). A Logger that accepts string | number is MORE capable than one that only accepts string — it can safely be used as a Logger. A Logger that only accepts the literal 'hello' is LESS capable — it would break if called with 'world'.
Q4: Type Widening
typescriptconst a = 'hello'; // type?
let b = 'hello'; // type?
const c = { x: 1, y: 2 }; // type of c.x?
const arr1 = [1, 2, 3]; // type?
const arr2 = [1, 2, 3] as const; // type?Answer:
a:'hello'(literal — const can't be reassigned)b:string(widened — let can be reassigned)c.x:number(object properties always widened even with const)arr1:number[]arr2:readonly [1, 2, 3](literal tuple)
Q5: never Propagation
typescripttype T1 = string & number; // ?
type T2 = never | string; // ?
type T3 = never & string; // ?
type T4 = Exclude<string, string>; // ?Answer: never, string, never, never
Why: string & number = impossible intersection = never. never | X = X (never contributes nothing to a union). never & X = never (never absorbs intersections). Exclude<string, string> removes all matching types = never.
Q6: Excess Property Checking
typescriptinterface Config { host: string; port: number; }
// Which ones error?
const c1: Config = { host: 'localhost', port: 3000, debug: true }; // ❌?
const obj = { host: 'localhost', port: 3000, debug: true };
const c2: Config = obj; // ❌?
function connect(config: Config) {}
connect({ host: 'localhost', port: 3000, debug: true }); // ❌?Answer: c1 ❌, c2 ✅, connect(...) ❌
Why: Excess property checks only happen with "fresh" object literals assigned directly to a typed variable or passed directly to a function. Assigning via a variable (obj) bypasses the check — TypeScript uses structural typing for variables.
Q7: Optional vs undefined
typescriptinterface A {
x?: string; // x is optional
}
interface B {
x: string | undefined; // x is required but can be undefined
}
const a: A = {}; // ✅
const b: B = {}; // ❌
const b2: B = { x: undefined }; // ✅
// With exactOptionalPropertyTypes: true
const a2: A = { x: undefined }; // ❌ (with flag) or ✅ (without)Answer: Depends on exactOptionalPropertyTypes. Without it, x?: string is x?: string | undefined — { x: undefined } is OK. With the flag, optional means "may be absent" but if present must be string.
Q8: keyof with Union vs Intersection
typescripttype A = { a: string; shared: number; };
type B = { b: string; shared: boolean; };
type U = keyof (A | B); // ?
type I = keyof (A & B); // ?Answer: U = 'shared', I = 'a' | 'b' | 'shared'
Why: keyof (A | B) = keys that exist on ALL members = 'shared' (only common key). keyof (A & B) = keys from either type = 'a' | 'b' | 'shared'. This is counterintuitive — union of types → intersection of keys, intersection of types → union of keys.
Q9: infer in Different Positions
typescripttype Head<T extends any[]> = T extends [infer H, ...any[]] ? H : never;
type Tail<T extends any[]> = T extends [any, ...infer T] ? T : never;
type Last<T extends any[]> = T extends [...any[], infer L] ? L : never;
type H = Head<[1, 2, 3]>; // ?
type T = Tail<[1, 2, 3]>; // ?
type L = Last<[1, 2, 3]>; // ?Answer: H = 1, T = [2, 3], L = 3
Why: infer captures the type at its position in the tuple. [infer H, ...any[]] captures the first element. [any, ...infer T] captures the rest (as a tuple). [...any[], infer L] captures the last element.
Q10: Recursive Types
typescripttype Json =
| string | number | boolean | null
| Json[]
| { [key: string]: Json };
const valid: Json = {
name: 'Alice',
scores: [1, 2, { extra: true }],
meta: null
};Q: Is this valid TypeScript? ✅ Yes — recursive type aliases are supported since TS 3.7.
Q11: typeof vs Type Annotations
typescriptclass UserService {
findById(id: string): Promise<User> { ... }
}
const service = new UserService();
type A = typeof UserService; // ?
type B = typeof service; // ?
type C = InstanceType<typeof UserService>; // ?Answer:
A = typeof UserService— the constructor type (class itself, not instance)B = UserService— instance type (same as explicit annotation)C = UserService— explicit way to get instance type from constructor type
Q12: Discriminated Union Exhaustiveness
typescripttype Shape =
| { kind: 'circle'; radius: number }
| { kind: 'square'; side: number };
function area(s: Shape): number {
if (s.kind === 'circle') return Math.PI * s.radius ** 2;
if (s.kind === 'square') return s.side ** 2;
// ❌ TypeScript doesn't error here by default!
}Q: How do you make TypeScript enforce exhaustiveness?
typescript// Solution 1: Add default with never assertion
function area(s: Shape): number {
if (s.kind === 'circle') return Math.PI * s.radius ** 2;
if (s.kind === 'square') return s.side ** 2;
const _never: never = s; // ❌ Error if a case is missing
return _never;
}
// Solution 2: switch with default
function area(s: Shape): number {
switch (s.kind) {
case 'circle': return Math.PI * s.radius ** 2;
case 'square': return s.side ** 2;
default:
const _never: never = s;
throw new Error(`Unknown shape: ${JSON.stringify(s)}`);
}
}Q13: Declaration Merging
typescriptinterface Point { x: number; }
interface Point { y: number; }
const p: Point = { x: 1 }; // ❌ or ✅?
const p2: Point = { x: 1, y: 2 }; // ❌ or ✅?Answer: p ❌, p2 ✅ — interfaces with same name merge. The merged Point requires both x and y.
Q14: Type Assertion Safety
typescriptconst x = 'hello' as unknown as number; // ❌ or ✅?
x.toFixed(2); // What happens?Answer: ✅ compiles — no error. Runtime TypeError when toFixed is called on a string.
Why: Type assertions (as) are compile-time only. The double-assertion via unknown bypasses TypeScript's safety check (which normally requires the types to overlap). This is an escape hatch but completely unsafe.
Q15: Function Overloads
typescriptfunction process(x: string): string;
function process(x: number): number;
function process(x: string | number): string | number {
return x;
}
const r1 = process('hello'); // type?
const r2 = process(42); // type?
const r3 = process(true); // ❌ or ✅?Answer: r1: string, r2: number, r3: ❌ error (no overload matches boolean).
Why: Overloads let TypeScript return specific types based on input types. The implementation signature is NOT exposed to callers — only the overload signatures are. Only types that match an overload signature are accepted.