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:
// 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 {} // interfaceinterface 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:
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:
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:
// 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 bothIntermediate 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:
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:
// 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 length8. 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:
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:
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 User10. What are mapped types?
Mapped types iterate over the keys of a type and produce a new type with transformed properties:
// 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:
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:
// 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:
// 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 type14. 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:
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 UserId15. 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):
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 safe16. 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:
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 string17. How do template literal types work?
Template literal types construct new string literal types by combining other types, mirroring JavaScript template literals:
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:
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 }; // NEW19. 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+:
// 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:
// 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:
// 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 readonly22. 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:
// 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()); // Date23. 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:
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)
// 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; // never25. 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:
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 pushQuick-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 |