Type Safety Skill
Ensuring runtime and compile-time type safety in TypeScript
🎯 STRICT MODE CONFIGURATION
Recommended tsconfig.json
json
{
"compilerOptions": {
// Enable all strict checks
"strict": true,
// Individual strict flags (all included in strict)
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"useUnknownInCatchVariables": true,
"alwaysStrict": true,
// Additional safety checks
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
// Module safety
"isolatedModules": true,
"verbatimModuleSyntax": true
}
}
Key Strict Options Explained
| Option | Effect |
|---|---|
strictNullChecks | null and undefined are distinct types |
noImplicitAny | Error on inferred any type |
noUncheckedIndexedAccess | Array/object index returns T | undefined |
exactOptionalPropertyTypes | ?: means missing, not undefined |
🛡️ RUNTIME VALIDATION
Using Zod
typescript
import { z } from 'zod';
// Define schema
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1).max(100),
email: z.string().email(),
age: z.number().int().positive().optional(),
role: z.enum(['user', 'admin', 'moderator']),
createdAt: z.date(),
});
// Infer type from schema
type User = z.infer<typeof UserSchema>;
// Validate at runtime
function createUser(input: unknown): User {
return UserSchema.parse(input); // Throws on invalid
}
// Safe validation
function tryCreateUser(input: unknown): User | null {
const result = UserSchema.safeParse(input);
return result.success ? result.data : null;
}
Using Valibot (Lighter Alternative)
typescript
import * as v from 'valibot';
const UserSchema = v.object({
id: v.pipe(v.string(), v.uuid()),
name: v.pipe(v.string(), v.minLength(1), v.maxLength(100)),
email: v.pipe(v.string(), v.email()),
role: v.picklist(['user', 'admin', 'moderator']),
});
type User = v.InferOutput<typeof UserSchema>;
// Validate
const result = v.safeParse(UserSchema, input);
if (result.success) {
const user = result.output; // Type-safe User
}
🔒 UNKNOWN VS ANY
typescript
// ❌ any - No type checking
function processAny(data: any) {
// All these compile but may crash at runtime
data.foo.bar.baz();
data.nonexistent();
const x: number = data; // No error!
}
// ✅ unknown - Requires narrowing
function processUnknown(data: unknown) {
// data.foo; // ❌ Error: Object is of type 'unknown'
if (typeof data === 'string') {
return data.toUpperCase(); // ✅ Narrowed to string
}
if (isUser(data)) {
return data.name; // ✅ Narrowed to User
}
throw new Error('Unexpected data type');
}
♾️ NEVER TYPE
Exhaustive Checks
typescript
type Status = 'pending' | 'active' | 'completed';
function handleStatus(status: Status): string {
switch (status) {
case 'pending':
return 'Waiting...';
case 'active':
return 'In progress';
case 'completed':
return 'Done!';
default:
// TypeScript will error if we miss a case
const _exhaustive: never = status;
throw new Error(`Unhandled status: ${_exhaustive}`);
}
}
// Utility for exhaustive checks
function assertNever(value: never, message?: string): never {
throw new Error(message ?? `Unexpected value: ${value}`);
}
Never in Conditional Types
typescript
// Filter type that removes certain types
type Filter<T, U> = T extends U ? never : T;
type NonString = Filter<string | number | boolean, string>;
// Result: number | boolean
// Extract only function properties
type FunctionKeys<T> = {
[K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];
✅ ASSERTION FUNCTIONS
typescript
// Assert condition
function assert(
condition: unknown,
message?: string
): asserts condition {
if (!condition) {
throw new Error(message ?? 'Assertion failed');
}
}
// Assert specific type
function assertIsString(
value: unknown
): asserts value is string {
if (typeof value !== 'string') {
throw new TypeError('Expected string');
}
}
// Usage
function processData(data: unknown) {
assertIsString(data);
// data is now string - TypeScript knows!
return data.toUpperCase();
}
🔄 BRANDED TYPES
typescript
// Create distinct types for same underlying type
declare const brand: unique symbol;
type Brand<T, B extends string> = T & { [brand]: B };
// Branded string types
type UserId = Brand<string, 'UserId'>;
type OrderId = Brand<string, 'OrderId'>;
// Factory functions
function createUserId(id: string): UserId {
// Validate format
if (!id.match(/^usr_/)) {
throw new Error('Invalid user ID format');
}
return id as UserId;
}
function createOrderId(id: string): OrderId {
if (!id.match(/^ord_/)) {
throw new Error('Invalid order ID format');
}
return id as OrderId;
}
// Can't mix them up!
function getUser(id: UserId) { /*...*/ }
const userId = createUserId('usr_123');
const orderId = createOrderId('ord_456');
getUser(userId); // ✅ OK
// getUser(orderId); // ❌ Error: OrderId not assignable to UserId
📦 DISCRIMINATED UNION SAFETY
typescript
// Ensure exhaustive handling
type ApiResponse<T> =
| { type: 'loading' }
| { type: 'success'; data: T }
| { type: 'error'; error: Error };
function handleResponse<T>(
response: ApiResponse<T>,
handlers: {
onLoading: () => void;
onSuccess: (data: T) => void;
onError: (error: Error) => void;
}
): void {
switch (response.type) {
case 'loading':
return handlers.onLoading();
case 'success':
return handlers.onSuccess(response.data);
case 'error':
return handlers.onError(response.error);
default:
assertNever(response);
}
}
🎯 CONST ASSERTIONS
typescript
// Without const assertion
const routes = {
home: '/',
about: '/about',
users: '/users',
};
// Type: { home: string; about: string; users: string }
// With const assertion
const routes = {
home: '/',
about: '/about',
users: '/users',
} as const;
// Type: { readonly home: '/'; readonly about: '/about'; readonly users: '/users' }
// Array const assertion
const statuses = ['pending', 'active', 'done'] as const;
// Type: readonly ['pending', 'active', 'done']
type Status = typeof statuses[number];
// Type: 'pending' | 'active' | 'done'
⚠️ COMMON TYPE ERRORS & FIXES
Error: Object is possibly undefined
typescript
// ❌ Problem
const user = users.find(u => u.id === id);
console.log(user.name); // Error!
// ✅ Solution 1: Optional chaining
console.log(user?.name);
// ✅ Solution 2: Guard clause
if (!user) throw new Error('User not found');
console.log(user.name);
Error: Type 'X' is not assignable to type 'Y'
typescript
// ❌ Problem
interface Cat { meow(): void }
interface Dog { bark(): void }
const pet: Cat = getDog(); // Error!
// ✅ Solution: Use union or proper type
const pet: Cat | Dog = getPet();
if ('meow' in pet) {
pet.meow();
}
Error: Argument of type 'string' is not assignable to type 'X'
typescript
// ❌ Problem
type Color = 'red' | 'blue' | 'green';
const input: string = getInput();
const color: Color = input; // Error!
// ✅ Solution: Type guard
function isColor(value: string): value is Color {
return ['red', 'blue', 'green'].includes(value);
}
if (isColor(input)) {
const color: Color = input; // ✅
}
📎 QUICK REFERENCE
| Pattern | Use Case |
|---|---|
unknown | External data, user input |
never | Exhaustive checks, impossible states |
| Zod/Valibot | Runtime validation with type inference |
| Branded types | Prevent ID/string mixups |
as const | Literal types, readonly objects |
| Assertion functions | Narrow types with validation |
🔗 RELATED SKILLS
- •typescript-expert - Core TypeScript patterns