Advanced TypeScript for Architecture

📖 Concept

TypeScript at the Staff level goes beyond basic type annotations. You're defining type systems that enforce architectural invariants at compile time — preventing entire categories of bugs before code even runs.

Key advanced patterns for React Native architecture:

  1. Generic Constraints — Ensuring type relationships between function parameters and return types
  2. Conditional Types — Types that change based on input types (T extends U ? X : Y)
  3. Mapped Types — Transforming existing types systematically
  4. Template Literal Types — String manipulation at the type level
  5. Discriminated Unions — Type-safe state machines (critical for RN navigation and state)
  6. Type Predicates — Custom type guards for runtime narrowing
  7. Branded/Nominal Types — Preventing primitive confusion (UserId vs PostId)
  8. Utility Types MasteryPartial, Required, Pick, Omit, Record, Extract, Exclude, ReturnType, Parameters, Awaited

Architectural impact of good TypeScript:

  • API contracts are enforced at build time — no runtime type mismatch crashes
  • Navigation parameters are type-safe — can't navigate to a screen with wrong params
  • Redux actions and state are exhaustively typed — impossible to dispatch a wrong action
  • Component props contracts prevent integration bugs between teams
  • Refactoring is safe — the compiler catches breaking changes across 200K LOC

💻 Code Example

codeTap to expand ⛶
1// === ADVANCED TYPESCRIPT FOR RN ARCHITECTURE ===
2
3// 1. Branded Types — prevent ID confusion
4type Brand<T, B> = T & { __brand: B };
5type UserId = Brand<string, 'UserId'>;
6type PostId = Brand<string, 'PostId'>;
7type OrderId = Brand<string, 'OrderId'>;
8
9function createUserId(id: string): UserId { return id as UserId; }
10function createPostId(id: string): PostId { return id as PostId; }
11
12function fetchUser(id: UserId): Promise<User> { /* ... */ }
13function fetchPost(id: PostId): Promise<Post> { /* ... */ }
14
15const userId = createUserId('u_123');
16const postId = createPostId('p_456');
17
18fetchUser(userId); // ✅
19// fetchUser(postId); // ❌ Compile error! Can't pass PostId where UserId expected
20
21// 2. Discriminated Unions for State Machines
22type NetworkState<T> =
23 | { status: 'idle' }
24 | { status: 'loading' }
25 | { status: 'success'; data: T }
26 | { status: 'error'; error: Error; retryCount: number };
27
28// Exhaustive handling — compiler ensures every case is handled
29function renderState<T>(state: NetworkState<T>) {
30 switch (state.status) {
31 case 'idle': return <EmptyState />;
32 case 'loading': return <Spinner />;
33 case 'success': return <DataView data={state.data} />; // data is typed!
34 case 'error': return <ErrorView error={state.error} />;
35 // If you add a new status, TypeScript errors until you handle it
36 }
37}
38
39// 3. Type-Safe Navigation (React Navigation)
40type RootStackParamList = {
41 Home: undefined;
42 Profile: { userId: UserId };
43 Post: { postId: PostId; showComments?: boolean };
44 Settings: undefined;
45};
46
47// Enforces correct params at every navigation call
48declare const navigation: NavigationProp<RootStackParamList>;
49navigation.navigate('Profile', { userId: createUserId('u_1') }); // ✅
50// navigation.navigate('Profile', { postId: 'p_1' }); // ❌ Wrong params!
51// navigation.navigate('Profle'); // ❌ Typo caught at compile time!
52
53// 4. Generic API Layer
54interface APIResponse<T> {
55 data: T;
56 meta: { page: number; total: number };
57}
58
59interface APIEndpoints {
60 '/users': { list: User[]; get: User; create: CreateUserDTO };
61 '/posts': { list: Post[]; get: Post; create: CreatePostDTO };
62 '/orders': { list: Order[]; get: Order; create: CreateOrderDTO };
63}
64
65type EndpointMethod = 'list' | 'get' | 'create';
66
67async function apiCall<
68 E extends keyof APIEndpoints,
69 M extends EndpointMethod
70>(
71 endpoint: E,
72 method: M,
73 ...args: M extends 'create' ? [APIEndpoints[E][M]] : []
74): Promise<APIResponse<APIEndpoints[E][M]>> {
75 // Implementation — fully type-safe API calls
76}
77
78// Usage — compiler knows exact return type
79const users = await apiCall('/users', 'list'); // APIResponse<User[]>
80const post = await apiCall('/posts', 'get'); // APIResponse<Post>
81// await apiCall('/users', 'delete'); // ❌ 'delete' not in EndpointMethod
82
83// 5. Conditional Types for Platform-Specific Code
84type PlatformSpecific<IOS, Android> =
85 typeof Platform.OS extends 'ios' ? IOS : Android;
86
87type HapticFeedback = PlatformSpecific<IOSHaptic, AndroidVibration>;
88
89// 6. Utility Type Composition
90type DeepPartial<T> = {
91 [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
92};
93
94type DeepRequired<T> = {
95 [P in keyof T]-?: T[P] extends object ? DeepRequired<T[P]> : T[P];
96};
97
98type DeepReadonly<T> = {
99 readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
100};
101
102// Use for configuration objects that are partially overridable
103type AppConfig = DeepRequired<{
104 api: { baseUrl: string; timeout: number; retries: number };
105 features: { darkMode: boolean; analytics: boolean };
106 cache: { maxSize: number; ttl: number };
107}>;
108
109type UserOverrides = DeepPartial<AppConfig>;

🏋️ Practice Exercise

TypeScript Architecture Exercises:

  1. Define a type-safe event emitter where event names and payloads are statically typed
  2. Create a createAction / createReducer pattern where the compiler ensures exhaustive action handling
  3. Implement a type-safe useQuery hook where the return type is inferred from the query function
  4. Define branded types for all ID types in your project — enforce them across the codebase
  5. Create a type-safe form validation system where field validators are typed to match the form schema
  6. Build a pipeline/compose function with correct type inference for each step

⚠️ Common Mistakes

  • Using any to 'fix' complex type errors — this defeats the entire purpose of TypeScript and hides real bugs

  • Over-engineering types — if a type definition is harder to understand than the code it protects, simplify it

  • Not using discriminated unions for state — using boolean flags (isLoading, isError, hasData) creates impossible states

  • Forgetting that TypeScript types are erased at runtime — you still need runtime validation for external data (API responses, user input)

  • Not leveraging generics for shared utilities — duplicating similar types instead of creating generic abstractions

💼 Interview Questions

🎤 Mock Interview

Mock interview is powered by AI for Advanced TypeScript for Architecture. Login to unlock this feature.