TypeScript Engineering Practice: From Type System to Project Architecture
TypeScript's type system is its most powerful weapon, yet most projects use less than half of its capability. This article covers four layers: the type system, error handling, project structure, and configuration strategy — for teams building or maintaining medium-to-large TypeScript projects.
The Bottom Line: TypeScript’s Value Is Not “Types” — It Is “Maintainability”
Many teams use TypeScript just because “everyone is using it,” but end up with any everywhere, type definitions out of sync with actual code, and TypeScript becoming “JavaScript with type annotations” — same bugs, just more code.
This article covers four layers: the type system, error handling, project structure, and configuration strategy.
1. Type System: Use Discriminated Unions and Type Narrowing
1.1 Discriminated Unions Instead of Optional Fields
The most common anti-pattern: an object type with all optional fields, checking existence at runtime.
// ❌ Anti-pattern
type ApiResponse = {
success?: boolean;
data?: any;
error?: string;
};
// ✅ Recommended: discriminated union
type ApiResponse<T> =
| { success: true; data: T }
| { success: false; error: string };
With discriminated unions, TypeScript infers available fields after narrowing — no manual checks needed.
const res: ApiResponse<User> = await fetchUser();
if (res.success) {
console.log(res.data.name); // ✅ TypeScript knows data exists
} else {
console.log(res.error); // ✅ TypeScript knows error exists
}
1.2 Type Narrowing Techniques
// typeof narrowing (primitives)
if (typeof x === 'string') { /* x: string */ }
// instanceof narrowing (class instances)
if (x instanceof Error) { /* x: Error */ }
// in operator narrowing (object properties)
if ('error' in x) { /* x: type with error property */ }
// Custom type guard
function isUser(x: any): x is User {
return x && typeof x.id === 'number' && typeof x.name === 'string';
}
2. Error Handling: Result Pattern Instead of throw
2.1 Why Not throw
The problem with throw: TypeScript does not force callers to handle exceptions. A function throws, but the caller might forget try-catch, and the program crashes at runtime.
2.2 Result Pattern Implementation
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
function parseJSON(json: string): Result<unknown> {
try {
return { ok: true, value: JSON.parse(json) };
} catch (e) {
return { ok: false, error: e as Error };
}
}
// Caller must handle both cases
const result = parseJSON('{"name":"test"}');
if (result.ok) {
console.log(result.value); // ✅ Type-safe
} else {
console.error(result.error.message); // ✅ Must handle error
}
2.3 Top-Level Unification
Convert Result to responses at the top level:
app.get('/users', async (req, res) => {
const result = await getUsers();
if (!result.ok) {
return res.status(500).json({ error: result.error.message });
}
res.json(result.value);
});
3. Project Structure: Layered Type Definitions
3.1 Recommended Directory Structure
src/
types/ # Global shared types
user.ts
api.ts
common.ts
api/ # API layer
types.ts # API types (auto-generated)
client.ts
utils/ # Utilities
result.ts # Result type definition
components/ # Components
UserCard.tsx
3.2 Type Files Are Types Only
Do not write implementation code in type files. Keep type files and implementation files separate.
4. Configuration Strategy
{
"compilerOptions": {
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"exactOptionalPropertyTypes": true,
"noUncheckedIndexedAccess": true,
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler"
}
}
Summary
| Layer | Key Principle | Common Mistake |
|---|---|---|
| Type system | Discriminated unions over optional fields | any everywhere |
| Error handling | Result pattern over throw | Missing try-catch |
| Project structure | Types separate from implementation | Implementation in type files |
| Configuration | strict: true | Disabling strict checks one by one |
TypeScript’s value is not “writing types” — it is “types helping you find bugs you did not think of.” With strict mode, discriminated unions, and Result patterns, TypeScript’s benefits far outweigh its learning cost.
Need TypeScript project setup or consulting? Contact us — tell us about your project scale and team, feasibility within 24 hours.
Related reading
- How to Run a Technical Review — technical decision-making for TypeScript adoption
- Frontend Performance Optimization — performance optimization for TypeScript projects
FAQ
Should we ever use the any type?
Yes, but with clear boundaries. Rules: ① When interacting with third-party libraries without type definitions, use any but confine it to the module boundary — do not let it leak into business code; ② When migrating from JavaScript, use any as a transitional step, gradually replacing with concrete types; ③ Never use any as a return type — it poisons type inference for all callers. Prefer unknown over any, because unknown requires type narrowing before access, which is safer.
What is the most important tsconfig.json option?
strict: true. It enables strictNullChecks, noImplicitAny, strictFunctionTypes, and several other checks simultaneously, catching a large class of potential bugs. Additionally, enable noUnusedLocals and noUnusedParameters to eliminate dead code, and exactOptionalPropertyTypes to prevent optional property typos.
Where should type definitions be placed?
Layered approach: ① Global shared types go in src/types/, organized by module; ② Local types for components or functions stay co-located, not in global files; ③ API-related types go in api/types.ts, ideally auto-generated from OpenAPI specs; ④ Never write implementation code in type files — type files are types only.
What error handling pattern should TypeScript projects use?
The Result pattern (discriminated union) instead of throw. Define type Result<T, E = Error> = { ok: true; value: T } | { ok: false; error: E }. Every fallible function returns a Result type. Callers must handle both ok and error cases — no missed error handling. At the top level (e.g., API route handlers), convert Result to HTTP responses uniformly.
This article comes from AI Enable Harness front-line delivery practice. Need a similar system or optimization service?
Subscribe to Updates
Get notified when new articles are published. No spam, occasional updates only.
Subscribe →