Skip to main content

📘 TypeScript System Design Decisions (Frontend – Senior Level)

Table of Contents

Part 1: System Design Principles

  1. What "System Design" Means in Frontend + TypeScript
  2. Core Principle: Types at Boundaries
  3. Preventing Impossible States
  4. API Layer Design in TypeScript
  5. Designing Reusable Hooks
  6. Designing Component APIs
  7. Type Organization Strategy
  8. When to Use interface vs type
  9. Strictness Strategy
  10. Avoiding Over-Engineering

Part 2: Real Frontend Interview Coding Problems


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 = false and data = undefined
  • data and error can 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/catch abuse
  • 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)

/types
api.ts
domain.ts
ui.ts
  • api.ts → server contracts
  • domain.ts → business models
  • ui.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 any only 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.