| name | typescript-type-safety |
| description | Use when encountering TypeScript any types, type errors, or lax type checking - eliminates type holes and enforces strict type safety through proper interfaces, type guards, and module augmentation |
TypeScript Type Safety
Overview
Zero tolerance for any types. Every any is a runtime bug waiting to happen.
Replace any with proper types using interfaces, unknown with type guards, or generic constraints. Use @ts-expect-error with explanation only when absolutely necessary.
When to Use
Use when you see:
: anyin function parameters or return typesas anytype assertions- TypeScript errors you're tempted to ignore
- External libraries without proper types
- Catch blocks with implicit
any
Don't use for:
- Already properly typed code
- Third-party
.d.tsfiles (contribute upstream instead)
Type Safety Hierarchy
Prefer in this order:
- Explicit interface/type definition
- Generic type parameters with constraints
- Union types
unknown(with type guards)never(for impossible states)
Never use: any
Quick Reference
| Pattern | Bad | Good |
|---|---|---|
| Error handling | catch (error: any) |
catch (error) { if (error instanceof Error) ... } |
| Unknown data | JSON.parse(str) as any |
const data = JSON.parse(str); if (isValid(data)) ... |
| Type assertions | (request as any).user |
(request as AuthRequest).user |
| Double casting | return data as unknown as Type |
Align interfaces instead: make types compatible |
| External libs | const server = fastify() as any |
declare module 'fastify' { ... } |
| Generics | function process(data: any) |
function process<T extends Record<string, unknown>>(data: T) |
Implementation
Error Handling
// ❌ BAD
try {
await operation();
} catch (error: any) {
console.error(error.message);
}
// ✅ GOOD - Use unknown and type guard
try {
await operation();
} catch (error) {
if (error instanceof Error) {
console.error(error.message);
} else {
console.error('Unknown error:', String(error));
}
}
// ✅ BETTER - Helper function
function toError(error: unknown): Error {
if (error instanceof Error) return error;
return new Error(String(error));
}
try {
await operation();
} catch (error) {
const err = toError(error);
console.error(err.message);
}
Unknown Data Validation
// ❌ BAD
const data = await response.json() as any;
console.log(data.user.name);
// ✅ GOOD - Type guard
interface UserResponse {
user: {
name: string;
email: string;
};
}
function isUserResponse(data: unknown): data is UserResponse {
return (
typeof data === 'object' &&
data !== null &&
'user' in data &&
typeof data.user === 'object' &&
data.user !== null &&
'name' in data.user &&
typeof data.user.name === 'string'
);
}
const data = await response.json();
if (isUserResponse(data)) {
console.log(data.user.name); // Type-safe
}
Module Augmentation
// ❌ BAD
const user = (request as any).user;
const db = (server as any).pg;
// ✅ GOOD - Augment third-party types
import { FastifyRequest, FastifyInstance } from 'fastify';
interface AuthUser {
user_id: string;
username: string;
email: string;
}
declare module 'fastify' {
interface FastifyRequest {
user?: AuthUser;
}
interface FastifyInstance {
pg: PostgresPlugin;
}
}
// Now type-safe everywhere
const user = request.user; // AuthUser | undefined
const db = server.pg; // PostgresPlugin
Generic Constraints
// ❌ BAD
function merge(a: any, b: any): any {
return { ...a, ...b };
}
// ✅ GOOD - Constrained generic
function merge<
T extends Record<string, unknown>,
U extends Record<string, unknown>
>(a: T, b: U): T & U {
return { ...a, ...b };
}
Type Alignment (Avoid Double Casts)
// ❌ BAD - Double cast indicates misaligned types
interface SearchPackage {
id: string;
type: string; // Too loose
}
interface RegistryPackage {
id: string;
type: PackageType; // Specific enum
}
return data.packages as unknown as RegistryPackage[]; // Hiding incompatibility
// ✅ GOOD - Align types from the source
interface SearchPackage {
id: string;
type: PackageType; // Use same specific type
}
interface RegistryPackage {
id: string;
type: PackageType; // Now compatible
}
return data.packages; // No cast needed - types match
Rule: If you need as unknown as Type, your interfaces are misaligned. Fix the root cause, don't hide it with double casts.
Common Mistakes
| Mistake | Why It Fails | Fix |
|---|---|---|
Using any for third-party libs |
Loses all type safety | Use module augmentation or @types/* package |
as any for complex types |
Hides real type errors | Create proper interface or use unknown |
as unknown as Type double casts |
Misaligned interfaces | Align types at source - same enums/unions |
| Skipping catch block types | Unsafe error access | Use unknown with type guards or toError helper |
| Generic functions without constraints | Allows invalid operations | Add extends constraint |
Ignoring ts-ignore accumulation |
Tech debt compounds | Fix root cause, use @ts-expect-error with comment |
TSConfig Strict Settings
Enable all strict options for maximum type safety:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
}
}
Type Audit Workflow
- Find:
grep -r ": any\|as any" --include="*.ts" src/ - Categorize: Group by pattern (errors, requests, external libs)
- Define: Create interfaces/types for each category
- Replace: Systematic replacement with proper types
- Validate:
npm run buildmust succeed - Test: All tests must pass
Real-World Impact
Before type safety:
- Runtime errors from undefined properties
- Silent failures from type mismatches
- Hours debugging production issues
- Difficult refactoring
After type safety:
- Errors caught at compile time
- IntelliSense shows all available properties
- Confident refactoring with compiler help
- Self-documenting code
Remember: Type safety isn't about making TypeScript happy - it's about preventing runtime bugs. Every any you eliminate is a production bug you prevent.