📘 TypeScript System Design Decisions (Frontend – Senior Level)
Table of Contents
Part 1: System Design Principles
- What "System Design" Means in Frontend + TypeScript
- Core Principle: Types at Boundaries
- Preventing Impossible States
- API Layer Design in TypeScript
- Designing Reusable Hooks
- Designing Component APIs
- Type Organization Strategy
- When to Use interface vs type
- Strictness Strategy
- Avoiding Over-Engineering
Part 2: Real Frontend Interview Coding Problems
- Problem 1: Type-Safe API Fetcher
- Problem 2: Event System with Type Safety
- Problem 3: Discriminated Reducer
- Problem 4: Generic List Component
- Problem 5: Safe Object Access
- Problem 6: Form Validation Result
- What Interviewers Evaluate
- Final Senior-Level Summary
1. What "System Design" Means in Frontend + TypeScript
In frontend interviews, TypeScript system design does not mean backend architecture.
It means:
- How you model data and state
- How you prevent invalid states
- How you scale types with app growth
- How you design APIs between components, hooks, services
- How you balance safety vs complexity
Interviewers evaluate:
- Thought process
- Trade-offs
- Maintainability
2. Core Principle: Types at Boundaries
Boundaries are:
- API responses
- Component props
- Hook return values
- Redux / store slices
- Form schemas
Rule
Be strict at boundaries, flexible internally.
Example: API Boundary
type ApiUser = {
id: string;
name: string;
created_at: string;
};
Convert once:
type User = {
id: string;
name: string;
createdAt: Date;
};
function mapUser(api: ApiUser): User {
return {
id: api.id,
name: api.name,
createdAt: new Date(api.created_at),
};
}
Why this matters:
- Prevents date bugs everywhere
- Centralized conversion
- Cleaner UI code
3. Preventing Impossible States (Most Important)
❌ Bad State Design
type State = {
loading: boolean;
data?: User[];
error?: string;
};
Problem:
loading = falseanddata = undefineddataanderrorcan exist together
✅ Correct State Design (Discriminated Union)
type State =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: User[] }
| { status: "error"; error: string };
Benefits:
- Impossible states eliminated
- Compiler enforces correctness
- Cleaner UI logic
4. API Layer Design in TypeScript
API Response Pattern
type ApiSuccess<T> = {
success: true;
data: T;
};
type ApiFailure = {
success: false;
error: string;
};
type ApiResponse<T> = ApiSuccess<T> | ApiFailure;
Usage:
function handleResponse(res: ApiResponse<User>) {
if (res.success) {
res.data.name;
} else {
res.error;
}
}
Why interviewers like this:
- Forces error handling
- No
try/catchabuse - Clear control flow
5. Designing Reusable Hooks (Typed Correctly)
Bad Hook Design
function useUser() {
return { data: null, loading: false };
}
Good Hook Design
type UseUserResult =
| { status: "loading" }
| { status: "success"; user: User }
| { status: "error"; error: string };
function useUser(): UseUserResult {
// implementation
}
Why:
- Consumer must handle all cases
- No undefined checks
- Scales well
6. Designing Component APIs (Senior Signal)
Bad Component API
<Button disabled={true} loading={true} />
Ambiguous:
- Can be disabled AND loading
Good Component API
type ButtonProps =
| { state: "idle"; onClick: () => void }
| { state: "loading" }
| { state: "disabled" };
function Button(props: ButtonProps) {}
Result:
- Invalid combinations impossible
- Better UX guarantees
7. Type Organization Strategy (Large Codebases)
Recommended Structure
/types
api.ts
domain.ts
ui.ts
api.ts→ server contractsdomain.ts→ business modelsui.ts→ component-level types
Why:
- Clear ownership
- Easier refactors
- Faster onboarding
8. When to Use interface vs type (System-Level Decision)
Use interface when:
- Public APIs
- Object shapes
- Library exports
Use type when:
- Unions
- Intersections
- Conditional types
- Utility types
Interview one-liner:
Interfaces describe shapes, types describe logic.
9. Strictness Strategy
Always Enable
{
"strict": true,
"strictNullChecks": true,
"noImplicitAny": true
}
Exception Handling
- Allow
anyonly at external boundaries - Wrap unsafe values with validators
10. Avoiding Over-Engineering (Very Important)
Senior engineers know when not to use complex types.
Bad:
type DeepMapped<T> = {
[K in keyof T]: T[K] extends object ? DeepMapped<T[K]> : T[K];
};
Good:
interface User {
id: string;
name: string;
}
Rule:
Types should help humans first, compiler second.
📘 Real Frontend Interview Coding Problems (TypeScript)
Problem 1: Type-Safe API Fetcher
Question
Create a typed fetcher that:
- Returns data on success
- Returns error on failure
- Is generic
Solution
type FetchSuccess<T> = {
ok: true;
data: T;
};
type FetchError = {
ok: false;
error: string;
};
type FetchResult<T> = FetchSuccess<T> | FetchError;
async function fetcher<T>(url: string): Promise<FetchResult<T>> {
try {
const res = await fetch(url);
if (!res.ok) {
return { ok: false, error: "Request failed" };
}
const data = await res.json();
return { ok: true, data };
} catch {
return { ok: false, error: "Network error" };
}
}
Problem 2: Event System with Type Safety
Question
Implement a typed event emitter.
Solution
type Events = {
login: { userId: string };
logout: undefined;
};
function emit<K extends keyof Events>(event: K, payload: Events[K]) {}
Usage:
emit("login", { userId: "123" });
emit("logout", undefined);
Problem 3: Discriminated Reducer
Question
Write a reducer with exhaustive checks.
Solution
type Action =
| { type: "ADD"; value: number }
| { type: "REMOVE"; value: number };
function reducer(state: number, action: Action): number {
switch (action.type) {
case "ADD":
return state + action.value;
case "REMOVE":
return state - action.value;
default:
const _exhaustive: never = action;
return state;
}
}
Problem 4: Generic List Component
Question
Create a reusable typed list component.
Solution
type ListProps<T> = {
items: T[];
renderItem: (item: T) => string;
};
function List<T>({ items, renderItem }: ListProps<T>) {
return items.map(renderItem).join(",");
}
Problem 5: Safe Object Access
Question
Create a function to safely access object keys.
Solution
function getValue<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
Usage:
getValue({ name: "Sayan" }, "name"); // string
Problem 6: Form Validation Result
Question
Model a form validation response.
Solution
type ValidationResult =
| { valid: true }
| { valid: false; errors: Record<string, string> };
Usage:
function submit(result: ValidationResult) {
if (!result.valid) {
result.errors.email;
}
}
11. What Interviewers Evaluate in These Problems
They look for:
- Correct type modeling
- No
any - Exhaustive handling
- Clean APIs
- Readability
Not:
- Clever tricks
- Over-engineered generics
- Complex conditional types without need
12. Final Senior-Level Summary
Strong frontend engineers use TypeScript to design correct systems, not just to fix errors.