Quick answer
TypeScript interview prep should focus on real code safety and API design, not only type trivia. Great answers explain how types improve maintainability, team velocity, and confidence during refactors.
Start here: Frontend Engineer Interview Prep
Read this next
Why TypeScript Matters in Interviews
TypeScript has become the industry standard for large-scale JavaScript applications. Companies like Microsoft, Google, Airbnb, and Slack use TypeScript extensively, making it a crucial skill for modern developers. Understanding TypeScript deeply demonstrates your commitment to code quality and maintainability.
Basic Types and Type Annotations
TypeScript provides static type checking by allowing you to annotate your code with types.
// Primitive types
let name: string = "John";
let age: number = 30;
let isActive: boolean = true;
let nothing: null = null;
let notDefined: undefined = undefined;
// Arrays
let numbers: number[] = [1, 2, 3];
let strings: Array<string> = ["a", "b", "c"];
// Tuple-fixed length array with specific types
let tuple: [string, number] = ["hello", 42];
// Enum
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT"
}
// Any-opt out of type checking (avoid when possible)
let flexible: any = "could be anything";
// Unknown-safer alternative to any
let uncertain: unknown = "need to check type before use";
if (typeof uncertain === "string") {
console.log(uncertain.toUpperCase()); // Now TypeScript knows it's a string
}
// Void-absence of return value
function logMessage(message: string): void {
console.log(message);
}
// Never-function never returns (throws or infinite loop)
function throwError(message: string): never {
throw new Error(message);
}
Practice this with Interview Masters
Use Interview Masters to generate TypeScript-heavy practice rounds that mix language depth with frontend design and debugging prompts.
Interfaces vs Type Aliases
Both interfaces and type aliases can define object shapes, but they have different capabilities.
// Interface-can be extended and merged
interface User {
id: number;
name: string;
email: string;
}
interface Admin extends User {
permissions: string[];
}
// Interface merging (declaration merging)
interface User {
age?: number; // Added to original User interface
}
// Type alias-more flexible, can represent any type
type ID = string | number;
type Point = {
x: number;
y: number;
};
// Type alias with intersection (similar to extends)
type AdminUser = User & {
permissions: string[];
};
// Type alias for function
type GreetFunction = (name: string) => string;
// Type alias for union types (can't do this with interface)
type Status = "pending" | "approved" | "rejected";
// Type alias for mapped types
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
When to use which?
- Use interfaces for object shapes, especially when you expect them to be extended or implemented by classes
- Use type aliases for unions, intersections, primitives, tuples, and complex type manipulations
Generics: Writing Reusable Type-Safe Code
Generics allow you to create reusable components that work with multiple types while maintaining type safety.
// Generic function
function identity<T>(arg: T): T {
return arg;
}
const str = identity<string>("hello"); // type: string
const num = identity(42); // type: number (inferred)
// Generic with constraints
interface Lengthwise {
length: number;
}
function logLength<T extends Lengthwise>(arg: T): T {
console.log(arg.length); // Now we know T has length
return arg;
}
logLength("hello"); // Works-string has length
logLength([1, 2, 3]); // Works-array has length
logLength({ length: 10 }); // Works-object has length property
// logLength(42); // Error-number doesn't have length
// Generic interface
interface Repository<T> {
getById(id: string): Promise<T>;
getAll(): Promise<T[]>;
create(item: T): Promise<T>;
update(id: string, item: Partial<T>): Promise<T>;
delete(id: string): Promise<void>;
}
// Generic class
class Queue<T> {
private items: T[] = [];
enqueue(item: T): void {
this.items.push(item);
}
dequeue(): T | undefined {
return this.items.shift();
}
peek(): T | undefined {
return this.items[0];
}
}
const numberQueue = new Queue<number>();
numberQueue.enqueue(1);
numberQueue.enqueue(2);
const first = numberQueue.dequeue(); // type: number | undefined
// Multiple type parameters
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
const p = pair("hello", 42); // type: [string, number]
// Generic with default type
interface ApiResponse<T = unknown> {
data: T;
status: number;
message: string;
}
Utility Types
TypeScript provides several built-in utility types for common type transformations.
interface User {
id: number;
name: string;
email: string;
password: string;
}
// Partial-makes all properties optional
type PartialUser = Partial<User>;
// { id?: number; name?: string; email?: string; password?: string; }
// Required-makes all properties required
type RequiredUser = Required<PartialUser>;
// Readonly-makes all properties readonly
type ReadonlyUser = Readonly<User>;
// Pick-select specific properties
type UserCredentials = Pick<User, "email" | "password">;
// { email: string; password: string; }
// Omit-remove specific properties
type PublicUser = Omit<User, "password">;
// { id: number; name: string; email: string; }
// Record-create object type with specific key and value types
type UserRoles = Record<string, string[]>;
// { [key: string]: string[]; }
// Exclude-remove types from union
type Status = "pending" | "approved" | "rejected";
type ActiveStatus = Exclude<Status, "rejected">;
// "pending" | "approved"
// Extract-keep only matching types from union
type MatchedStatus = Extract<Status, "pending" | "archived">;
// "pending"
// NonNullable-remove null and undefined
type MaybeString = string | null | undefined;
type DefiniteString = NonNullable<MaybeString>;
// string
// ReturnType-get return type of function
function createUser(name: string): User {
return { id: 1, name, email: "", password: "" };
}
type CreateUserReturn = ReturnType<typeof createUser>;
// User
// Parameters-get parameter types as tuple
type CreateUserParams = Parameters<typeof createUser>;
// [name: string]
Advanced Type Patterns
// Conditional types
type IsString<T> = T extends string? true: false;
type A = IsString<string>; // true
type B = IsString<number>; // false
// Distributive conditional types
type ToArray<T> = T extends any? T[]: never;
type StrOrNumArray = ToArray<string | number>;
// string[] | number[]
// infer keyword-extract type within conditional
type UnwrapPromise<T> = T extends Promise<infer U>? U: T;
type Unwrapped = UnwrapPromise<Promise<string>>; // string
// Mapped types
type Nullable<T> = {
[P in keyof T]: T[P] | null;
};
type NullableUser = Nullable<User>;
// Template literal types
type EventName = "click" | "focus" | "blur";
type EventHandler = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur"
// Recursive types
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object
? DeepReadonly<T[P]>
: T[P];
};
// Type guards
function isString(value: unknown): value is string {
return typeof value === "string";
}
function processValue(value: unknown) {
if (isString(value)) {
console.log(value.toUpperCase()); // TypeScript knows it's a string
}
}
// Discriminated unions
interface Square {
kind: "square";
size: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
interface Circle {
kind: "circle";
radius: number;
}
type Shape = Square | Rectangle | Circle;
function getArea(shape: Shape): number {
switch (shape.kind) {
case "square":
return shape.size ** 2;
case "rectangle":
return shape.width * shape.height;
case "circle":
return Math. PI * shape.radius ** 2;
}
}
Practical Interview Examples
// Example 1: Type-safe API client
interface ApiClient<T> {
get(url: string): Promise<T>;
post(url: string, data: Partial<T>): Promise<T>;
}
function createApiClient<T>(baseUrl: string): ApiClient<T> {
return {
async get(url: string): Promise<T> {
const response = await fetch(`${baseUrl}${url}`);
return response.json();
},
async post(url: string, data: Partial<T>): Promise<T> {
const response = await fetch(`${baseUrl}${url}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
return response.json();
},
};
}
const userApi = createApiClient<User>("/api/users");
// Example 2: Type-safe event emitter
type EventMap = Record<string, any>;
type EventKey<T extends EventMap> = string & keyof T;
type EventCallback<T> = (params: T) => void;
class TypedEventEmitter<T extends EventMap> {
private listeners: { [K in keyof T]?: EventCallback<T[K]>[] } = {};
on<K extends EventKey<T>>(event: K, callback: EventCallback<T[K]>): void {
if (!this.listeners[event]) {
this.listeners[event] = [];
}
this.listeners[event]!.push(callback);
}
emit<K extends EventKey<T>>(event: K, params: T[K]): void {
this.listeners[event]?.forEach(callback => callback(params));
}
}
// Usage
interface AppEvents {
userLogin: { userId: string; timestamp: Date };
userLogout: { userId: string };
error: { message: string; code: number };
}
const emitter = new TypedEventEmitter<AppEvents>();
emitter.on("userLogin", ({ userId, timestamp }) => {
console.log(`User ${userId} logged in at ${timestamp}`);
});
Conclusion
TypeScript's type system is powerful and expressive. Master these concepts and you'll be well-prepared for any TypeScript interview question. Practice by typing your own projects and exploring the TypeScript handbook for more advanced patterns.
