TypeScript Interview Prep
Top 30 TypeScript interview questions and answers covering types, generics, advanced patterns, and real-world usage.
Q1: What is TypeScript and why use it?
TypeScript is a statically typed superset of JavaScript that compiles to plain JavaScript. It adds a type system on top of JavaScript’s runtime semantics, catching errors at compile time — before the code runs — rather than at runtime in production.
Key benefits: early error detection, better IDE tooling (autocomplete, safe refactoring), self-documenting code through types, and confidence when refactoring large codebases. The fact that it compiles to JavaScript means it works anywhere JavaScript does.
Q2: What is the difference between any and unknown?
Both can hold any value, but unknown is the safe version — it forces you to check the type before using it, while any opts out of type checking entirely. Use any sparingly as a last resort when migrating JavaScript. Use unknown for values whose type you genuinely don’t know yet, like API responses, JSON parsing results, and catch block errors.
let a: any = "hello";
a.toUpperCase(); // fine — no type check required, but you lose all safety
let u: unknown = "hello";
u.toUpperCase(); // Error — must narrow the type first
if (typeof u === "string") {
u.toUpperCase(); // fine — narrowed to string within this branch
}
Q3: What does strict: true enable?
strict: true is a shorthand that enables a bundle of strictness flags at once. It’s the most impactful single setting in tsconfig.json and should be turned on in every new project. The individual flags it enables are:
strictNullChecks—null/undefinedare not assignable to other typesnoImplicitAny— variables must have a type if inference yieldsanystrictFunctionTypes— stricter checking of function parameter typesstrictBindCallApply— typedbind,call, andapplystrictPropertyInitialization— class properties must be initializednoImplicitThis—thismust have a known type
Always use strict: true in new projects.
Q4: What is the difference between interface and type?
Both describe object shapes, but they differ in what else they can do and how they behave on conflicts. The general rule: use interface for object shapes and public API contracts; use type for unions, intersections, and type-level computations.
- Declaration merging: interfaces can be declared multiple times and TypeScript merges them. Type aliases cannot.
- Extends vs intersection: interfaces use
extends, types use&. - Unions: only
typecan represent union types (type A = B | C). - Computed/mapped types: only
typecan use mapped types and conditional types.
Q5: What are generics? Give a practical example.
Generics let you write reusable code that preserves type relationships across different types. Without generics, a function that works on arrays of any type would either take any[] (losing type information) or need a separate overload for every possible element type. With generics, you write the function once and the caller supplies the type.
// T is inferred from the array argument — no explicit annotation needed at the call site
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
first([1, 2, 3]); // returns number | undefined
first(["a", "b"]); // returns string | undefined
// Generic function that types the server response correctly
async function fetchJson<T>(url: string): Promise<T> {
const res = await fetch(url);
return res.json() as Promise<T>;
}
const user = await fetchJson<User>("/api/users/1"); // User, not any
Q6: What is a type guard?
A type guard narrows a union type within a conditional block. TypeScript recognizes several built-in forms (typeof, instanceof, in) and user-defined guards using the value is Type predicate syntax. The predicate tells TypeScript to narrow the variable to that type in the truthy branch of any conditional that calls the guard.
function isError(value: unknown): value is Error {
return value instanceof Error;
}
function handle(value: unknown): string {
if (isError(value)) {
return value.message; // TypeScript knows: Error here
}
return String(value);
}
The value is Error return type is the type predicate — it tells TypeScript to narrow value to Error in the truthy branch.
Q7: Explain discriminated unions.
A discriminated union is a union of types that share a common literal property — the discriminant. TypeScript uses the discriminant to narrow the type in switch statements and if chains, giving you access to the specific properties of each variant. This pattern is the foundation of typed state machines, API response modeling, and safe exhaustive checks.
type Result<T> =
| { status: "success"; data: T }
| { status: "error"; message: string }
| { status: "loading" };
function render<T>(result: Result<T>): string {
switch (result.status) {
case "success": return JSON.stringify(result.data); // data is available here
case "error": return `Error: ${result.message}`; // message is available here
case "loading": return "Loading...";
}
}
Q8: What does keyof do?
keyof T produces a union of the string/number/symbol keys of type T. It’s most useful when combined with indexed access types (T[K]) to write functions that are constrained to valid property names. This eliminates entire classes of runtime errors where code accesses a property that doesn’t exist on an object.
interface User { id: number; name: string; email: string; }
type UserKey = keyof User; // "id" | "name" | "email"
// K extends keyof T ensures the key exists, and T[K] gives the correct return type
function get<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
get(user, "name"); // returns string
get(user, "id"); // returns number
get(user, "foo"); // Error — "foo" is not a key of User
Q9: What is typeof in TypeScript types?
The type-level typeof extracts the TypeScript type of a value. It’s different from the runtime typeof operator — the runtime one returns a string like "string" or "object", while the type-level one produces a TypeScript type you can use in annotations and utilities. This is especially useful when a function or constant is the source of truth for a shape and you want to derive the type from it rather than defining it separately.
const config = { host: "localhost", port: 3000 };
type Config = typeof config; // { host: string; port: number }
function createUser() {
return { id: 1, name: "Alice" };
}
// Derive the type from the function rather than writing a separate interface
type User = ReturnType<typeof createUser>; // { id: number; name: string }
Q10: What are mapped types?
Mapped types iterate over the keys of a type to produce a new type, transforming each property in a uniform way. They’re the mechanism behind TypeScript’s built-in utility types — Partial, Required, Readonly, and Record are all implemented as mapped types. You can use them to build your own transformations.
// Transform every property in T to T[K] | null
type Nullable<T> = { [K in keyof T]: T[K] | null };
interface User { name: string; email: string; }
type NullableUser = Nullable<User>;
// { name: string | null; email: string | null }
TypeScript’s built-in utility types (Partial, Required, Readonly, Record) are all implemented as mapped types.
Q11: What is infer?
infer declares a type variable inside a conditional type that TypeScript fills in when the condition is evaluated. It lets you extract parts of a type — the element type of an array, the return type of a function, the resolved type of a Promise — without knowing the concrete type in advance. This is how ReturnType, Awaited, and Parameters are implemented in TypeScript’s standard library.
// Extracts the element type from an array type
type UnpackArray<T> = T extends (infer U)[] ? U : T;
type A = UnpackArray<string[]>; // string
type B = UnpackArray<number>; // number (passthrough — not an array)
// Extracts the return type of any function
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type C = ReturnType<() => boolean>; // boolean
Q12: Implement Partial<T> from scratch.
Partial<T> makes every property in T optional. It’s implemented as a mapped type that adds ? to every key. Understanding this implementation helps you write your own transformations when the built-in utilities don’t cover your use case.
// The ? modifier makes each property optional
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
interface User { id: number; name: string; email: string; }
type PartialUser = MyPartial<User>;
// { id?: number; name?: string; email?: string }
Q13: What is the never type used for?
never represents the type of values that can never exist. It has two main uses: typing functions that never return (they throw or loop forever), and exhaustive checks that prove a union is fully handled. The exhaustive check pattern is particularly valuable — it makes TypeScript report an error at compile time whenever you add a new union member but forget to handle it.
// Functions that throw always return never — they never produce a value
function assertNever(value: never): never {
throw new Error(`Unhandled value: ${JSON.stringify(value)}`);
}
type Direction = "left" | "right";
function handle(d: Direction): void {
if (d === "left") { /* ... */ return; }
if (d === "right") { /* ... */ return; }
// After handling all members, d should be never
// TypeScript errors here if a new Direction is added but not handled
assertNever(d);
}
Q14: What is declaration merging?
Declaration merging means that multiple declarations of the same interface are automatically combined into one. This is particularly useful for augmenting types from third-party libraries without forking their type definitions — you add properties to Express’s Request, or extend a library’s options interface, by declaring the same interface in your own file.
// TypeScript merges these two declarations into one Animal type
interface Animal { name: string; }
interface Animal { age: number; }
const a: Animal = { name: "Rex", age: 3 }; // must satisfy both declarations
Most useful for augmenting third-party types (e.g., adding user to Express’s Request).
Q15: What is as const?
as const tells TypeScript to treat a value as deeply immutable and to infer the most specific (narrowest) types possible — string literal types instead of string, number literal types instead of number. Without as const, TypeScript widens literal values to their base types. This is essential when you want to use the values of an array or object as a union type.
const config = { env: "prod", port: 3000 } as const;
// type: { readonly env: "prod"; readonly port: 3000 }
// Without as const: { env: string; port: number }
const ROLES = ["admin", "user", "viewer"] as const;
// Extract the union of array values as a type
type Role = (typeof ROLES)[number]; // "admin" | "user" | "viewer"
Q16: What is the satisfies operator?
satisfies (TypeScript 4.9+) validates that a value conforms to a type without widening the inferred type. The difference from a type annotation is subtle but important: a type annotation replaces the inferred type with the annotation type (losing specificity), while satisfies validates against the type but preserves the narrower inferred type. This is useful when you want both the safety of type checking and the precision of inference.
type Colors = Record<string, string | [number, number, number]>;
// satisfies checks the shape against Colors but preserves the specific types
const palette = {
red: [255, 0, 0],
green: "#00ff00",
} satisfies Colors;
// With satisfies, TypeScript knows the specific types:
palette.red; // [number, number, number] — not string | [...]
palette.green; // string — not string | [...]
palette.green.toUpperCase(); // works — TypeScript knows it's a string, not an array
Q17: What is a conditional type?
A conditional type is a type-level ternary: T extends U ? X : Y. If T is assignable to U, the type resolves to X; otherwise Y. When the checked type is a naked type parameter (not wrapped in anything), the condition distributes over union members — each member is checked individually and the results are unioned together.
type IsString<T> = T extends string ? "yes" : "no";
type A = IsString<string>; // "yes"
type B = IsString<number>; // "no"
// Distribution over unions — each member is checked separately
type ToArray<T> = T extends any ? T[] : never;
type C = ToArray<string | number>; // string[] | number[]
Q18: What is the difference between Exclude and Omit?
They operate on different kinds of types and are not interchangeable. Exclude works on union types and removes members; Omit works on object types and removes properties. Mixing them up — using Omit on a union or Exclude on an object — produces never or unexpected types rather than an error, so it’s worth keeping the distinction clear.
// Exclude — removes members from a union type
type Status = "active" | "inactive" | "banned";
type SafeStatus = Exclude<Status, "banned">; // "active" | "inactive"
// Omit — removes properties from an object type
interface User { id: number; name: string; password: string; }
type SafeUser = Omit<User, "password">; // { id: number; name: string }
Q19: What is covariance and contravariance?
Variance describes how subtype relationships flow through type constructors. Return types are covariant — a function returning Dog is assignable where a function returning Animal is expected, because Dog is a subtype of Animal. Parameter types are contravariant — a function accepting Animal is assignable where a function accepting Dog is expected, because a function that can handle any animal can certainly handle a dog.
// Covariance — return types: Dog is assignable to Animal
type AnimalFn = () => Animal;
type DogFn = () => Dog;
let af: AnimalFn = (() => new Dog()); // fine — Dog is a subtype of Animal
// Contravariance — parameter types: Animal handler is assignable where Dog handler is expected
type HandleAnimal = (a: Animal) => void;
type HandleDog = (d: Dog) => void;
let handleAnimal: HandleAnimal = (a) => a.speak();
let handleDog: HandleDog = handleAnimal; // fine — a function that handles any animal handles dogs too
Q20: How do you type a function overload?
Function overloads let you define multiple call signatures for a single function — useful when a function’s return type depends on the types of its arguments. The overload signatures are what callers see; the implementation signature must be compatible with all overloads but is not directly visible to callers.
// These two signatures are what callers see
function format(value: string): string;
function format(value: number, decimals: number): string;
// The implementation signature must cover all overload cases
function format(value: string | number, decimals?: number): string {
if (typeof value === "string") return value.trim();
return value.toFixed(decimals ?? 2);
}
Q21: What is Pick vs Extract?
Like Omit vs Exclude, these operate on different kinds of types. Pick selects properties from an object type; Extract selects members from a union type. Both are the “keep” counterparts to Omit and Exclude.
// Pick — keeps named properties from an object type
interface User { id: number; name: string; email: string; }
type NameAndEmail = Pick<User, "name" | "email">;
// { name: string; email: string }
// Extract — keeps matching members from a union type
type Status = "active" | "inactive" | "pending";
type FinalStatus = Extract<Status, "active" | "inactive">;
// "active" | "inactive"
Q22: How do you type a class constructor?
Typing a constructor as a value (rather than typing an instance) uses the new (...args) => T syntax. This is the foundation of mixin patterns — functions that take a class constructor and return a new class that extends it, adding properties or methods to every class they’re applied to.
type Constructor<T = {}> = new (...args: any[]) => T;
// A mixin that adds a createdAt timestamp to any class
function Timestamped<TBase extends Constructor>(Base: TBase) {
return class extends Base {
createdAt = new Date(); // added to every instance
};
}
class User {
constructor(public name: string) {}
}
const TimestampedUser = Timestamped(User);
const user = new TimestampedUser("Alice");
user.createdAt; // Date — TypeScript knows about the mixin-added property
Q23: What is NonNullable<T>?
NonNullable<T> removes null and undefined from a type, producing the type of the definite value. It’s implemented as a conditional type and useful whenever you’ve done a null check and want to express the narrowed type in a utility or helper function.
type MaybeString = string | null | undefined;
type DefiniteString = NonNullable<MaybeString>; // string
// Implementation — the conditional distributes over the union members
type NonNullable<T> = T extends null | undefined ? never : T;
Q24: What are template literal types?
Template literal types build string types using the same backtick syntax as JavaScript template literals. They’re useful for generating unions of string patterns — CSS property names, event names, API endpoint paths — without listing every combination manually. They can be combined with union types to produce the Cartesian product of the string members.
type Direction = "top" | "right" | "bottom" | "left";
// Capitalize is a built-in string transformation type
type PaddingProp = `padding${Capitalize<Direction>}`;
// "paddingTop" | "paddingRight" | "paddingBottom" | "paddingLeft"
Q25: What does Awaited<T> do?
Awaited<T> recursively unwraps nested Promise types to get the final resolved value type. It handles Promise<Promise<T>> correctly — something ReturnType alone can’t do. It’s most useful for extracting the resolved type of async functions when you want to use that type elsewhere.
type A = Awaited<Promise<string>>; // string
type B = Awaited<Promise<Promise<number>>>; // number — unwraps both levels
type C = Awaited<string>; // string (passthrough for non-Promises)
// Extract the resolved type of an async function
async function getUser(): Promise<User> { /* ... */ return {} as User; }
type UserType = Awaited<ReturnType<typeof getUser>>; // User
Q26: What is the difference between interface extends and type &?
Both create a type with all properties of the parents, but they handle property name conflicts differently. interface extends reports a compile error on conflicting properties, which is usually what you want — it prevents accidentally creating an impossible type. Type intersection silently produces never for conflicting properties, which is harder to debug.
interface A { x: number; }
interface B { x: string; }
// interface conflict — compiler error: x is incompatible between A and B
interface C extends A, B {} // Error
// type intersection — x becomes never (impossible to satisfy both)
type C = A & B; // { x: never }
For object composition without conflicts, prefer interface extends.
Q27: How do you handle unknown errors in catch blocks?
In strict mode, caught errors are typed as unknown because JavaScript allows throwing any value. You must narrow the type before using it. Extracting this into a reusable helper keeps error handling consistent across the codebase.
try {
riskyOperation();
} catch (err) {
// err is unknown — must narrow before accessing properties
if (err instanceof Error) {
console.error(err.message);
} else {
console.error(String(err));
}
}
// Reusable helper that always produces a string message
function getErrorMessage(err: unknown): string {
if (err instanceof Error) return err.message;
return String(err);
}
Q28: What is ReturnType and when is it useful?
ReturnType<T> extracts the return type of a function type. It’s most valuable when a function is the authoritative source of a type’s shape — you derive the type from the function rather than writing a separate interface, so they can never drift apart. This works especially well for factory functions and ORM query results.
function createUser(name: string) {
return { id: Math.random(), name, createdAt: new Date() };
}
// The type is derived from the function — no separate interface needed
type User = ReturnType<typeof createUser>;
// { id: number; name: string; createdAt: Date }
Useful when a function is the source of truth for a type — you don’t need a separate interface.
Q29: What is Parameters<T>?
Parameters<T> extracts the parameter types of a function as a tuple. It’s the tool of choice when wrapping a function — you can type the wrapper’s parameters to exactly match the original without duplicating the signature. Pair it with ReturnType to build fully typed function wrappers.
function connect(host: string, port: number): void {}
type ConnectArgs = Parameters<typeof connect>; // [string, number]
// Wrapping a function while preserving its full signature
function withLogging<T extends (...args: any[]) => any>(fn: T) {
return (...args: Parameters<T>): ReturnType<T> => {
console.log("called with", args);
return fn(...args);
};
}
Q30: What is a branded type and why use it?
TypeScript uses structural typing — two types with the same shape are interchangeable, even if they represent different concepts. A branded type adds a phantom property that exists only in the type system to make structurally identical types nominally distinct. This prevents passing a UserId where a PostId is expected, even though both are plain numbers at runtime. The brand has zero runtime cost.
type UserId = number & { readonly _brand: "UserId" };
type PostId = number & { readonly _brand: "PostId" };
// Constructor functions that "brand" a plain number
function createUserId(n: number): UserId {
return n as UserId;
}
function getPost(id: PostId): Post { /* ... */ return {} as Post; }
const uid = createUserId(1);
const pid = 42 as PostId;
getPost(pid); // fine — PostId is assignable to PostId
getPost(uid); // Error — UserId is not assignable to PostId
getPost(42); // Error — plain number is not PostId
TypeScript’s structural type system normally treats two number types as identical. Branded types add nominal behavior without runtime overhead — the brand property exists only in the type system.