| name | typescript-patterns |
| description | WHAT: TypeScript type patterns and Zod schema-first validation. WHEN: defining types, validating API data, creating type guards, using generics. KEYWORDS: typescript, types, interface, zod, schema, validation, generics, discriminated unions, type guards, const assertions. |
TypeScript Patterns Standards
Standards for TypeScript type definitions, runtime validation with Zod, generics, and type safety patterns.
When to Use
Use these standards when:
- Defining types, interfaces, or type aliases
- Validating API responses or user input
- Creating generic functions or components
- Implementing state machines or reducers
- Writing type guards or utility types
- Ensuring type safety at runtime
Core Principles
Zero Tolerance for any
Never use any type. Use unknown with type guards or generics instead.
✅ Good:
const processData = (data: unknown) => {
if (isRecipeDetail(data)) {
return data.title;
}
throw new Error('Invalid data');
};
const processData = <T extends { value: string }>(data: T) => {
return data.value;
};
❌ Bad:
const processData = (data: any) => {
return data.value;
};
Why: any disables all type checking, defeating the purpose of TypeScript. unknown requires explicit type narrowing, catching errors at compile time.
Zod Schema-First Types
Define Zod schemas first, then infer TypeScript types. Single source of truth.
✅ Good:
import { z } from 'zod';
export const RecipeDetailSchema = z.object({
id: z.string().min(1, 'Recipe ID cannot be empty').trim(),
title: z.string().min(1, 'Recipe title cannot be empty').trim(),
heroImageUrl: z.string().url('Invalid hero image URL'),
savedDate: z.string().datetime({ offset: true }),
ingredients: z.array(RecipeIngredientSchema).min(1, 'At least one ingredient required'),
});
export type RecipeDetail = z.infer<typeof RecipeDetailSchema>;
export const isRecipeDetail = (obj: unknown): obj is RecipeDetail => {
return RecipeDetailSchema.safeParse(obj).success;
};
export const parseRecipeDetail = (obj: unknown) => {
return RecipeDetailSchema.safeParse(obj);
};
❌ Bad:
type RecipeDetail = {
id: string;
title: string;
};
const schema = z.object({ id: z.string(), title: z.string() });
Why: Schema-first ensures runtime validation matches TypeScript types. Prevents drift between validation and types.
Type Aliases vs Interfaces
Use type for unions and primitives. Use interface for object shapes that may be extended.
✅ Good:
export type ProductId = Scalars['ShoppableProductId']['output'];
export type Action = 'onDecreaseProduct' | 'onRemoveProduct' | 'onSwapCourse';
export type Source = 'LIST' | 'CAROUSEL' | 'WIDGET' | 'POPUP';
export interface UserProfileProps {
userId: string;
onEdit: () => void;
}
export type TrackingParams = {
recipePosition?: number;
widgetName?: string;
source?: Source;
topLayer?: TopLayer;
screenName?: ScreenName;
};
❌ Bad:
interface Action {
type: 'onDecreaseProduct' | 'onRemoveProduct';
}
type UserProfileProps = {
userId: string;
};
Why: Types are more flexible for unions/primitives. Interfaces enable declaration merging and better error messages for objects.
When to use type:
- Union types
- Type aliases for primitives
- Complex object shapes with mapped/conditional types
- Function types
When to use interface:
- Object shapes that may be extended
- Class contracts
- Declaration merging
Const Assertions Over Enums
Use as const for immutable constant objects instead of enums.
✅ Good:
export const SCREEN_NAME = {
HOME: 'Home',
STORE: 'Store',
UPSELL: 'Upsell',
} as const;
export type ScreenName = (typeof SCREEN_NAME)[keyof typeof SCREEN_NAME];
❌ Bad:
export const SCREEN_NAME = {
HOME: 'Home',
};
export enum ScreenName {
Home = 'Home',
Store = 'Store',
}
Why: as const creates readonly literal types with better type inference and tree-shaking. Enums generate runtime code.
Use enums only when:
- You need reverse mapping (enum value to key name)
- You have sequential numeric values with meaning
- You need to iterate over all values
Import Type Syntax
Use import type for type-only imports to reduce bundle size.
✅ Good:
import type { User, Subscription } from '@data-access/graphql';
import type { ComponentProps } from './types';
import { useQuery, type UseQueryOptions } from '@tanstack/react-query';
import { View, type ViewStyle } from 'react-native';
❌ Bad:
import { User, ComponentProps } from './types';
Why: import type is erased at runtime, reducing bundle size. Signals to readers these are type-only imports.
Discriminated Unions
Use discriminated unions for state machines and action types.
✅ Good:
type AsyncData<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error };
const renderData = (state: AsyncData<Recipe>) => {
switch (state.status) {
case 'idle':
return null;
case 'loading':
return <LoadingSpinner />;
case 'success':
return <RecipeList recipes={state.data} />;
case 'error':
return <ErrorMessage error={state.error} />;
}
};
❌ Bad:
type AsyncData<T> = {
loading: boolean;
data?: T;
error?: Error;
};
Why: Discriminated unions enable exhaustive type checking and prevent invalid state combinations (e.g., both data and error present).
Generics
Use generics for reusable type-safe functions and components.
✅ Good:
const findById = <T extends { id: string }>(items: T[], id: string): T | undefined => {
return items.find((item) => item.id === id);
};
type ListProps<T> = {
items: T[];
renderItem: (item: T) => ReactNode;
keyExtractor: (item: T) => string;
};
export const List = <T,>({ items, renderItem, keyExtractor }: ListProps<T>) => {
return (
<FlatList
data={items}
renderItem={({ item }) => renderItem(item)}
keyExtractor={(item) => keyExtractor(item)}
/>
);
};
❌ Bad:
const findById = (items: any[], id: string): any => {
return items.find((item) => item.id === id);
};
Why: Generics preserve type information through function calls without duplication or loss of type safety.
Utility Types
Use TypeScript utility types for type transformations.
Built-in Utility Types:
type UserCredentials = Pick<User, 'email' | 'password'>;
type UserWithoutPassword = Omit<User, 'password'>;
type PartialUser = Partial<User>;
type RequiredUser = Required<User>;
type ErrorMessages = Record<string, string>;
type QueryResult = ReturnType<typeof useRecipeQuery>;
type FetchParams = Parameters<typeof fetchRecipeDetail>;
Custom Utility Types:
export type Nullable<T> = T | null;
export type Optional<T> = T | undefined;
export type Maybe<T> = T | null | undefined;
Strict Null Checks
Handle null/undefined explicitly with optional chaining and nullish coalescing.
✅ Good:
type User = {
name: string;
email: string;
avatar?: string;
};
const getUserAvatar = (user: User): string => {
return user.avatar ?? 'default-avatar.png';
};
const userName = user?.profile?.name ?? 'Unknown';
❌ Bad:
const userName = user!.profile!.name;
Why: Strict null checks prevent null/undefined errors at compile time. Optional chaining and nullish coalescing handle undefined gracefully.
Function Type Annotations
Explicitly type function parameters and return types for public APIs.
✅ Good:
const calculateTotal = (items: Item[]): number => {
return items.reduce((sum, item) => sum + item.price, 0);
};
const fetchUser = async (userId: string): Promise<User | null> => {
try {
const response = await apiClient.getUser(userId);
return response.data;
} catch (error) {
return null;
}
};
❌ Bad:
const calculateTotal = (items: Item[]) => {
return items.reduce((sum, item) => sum + item.price, 0);
};
Why: Explicit return types serve as documentation and catch errors where return values don't match expectations.
Type Guards with Zod
Create type guards using Zod validation for runtime safety.
✅ Good:
export const isRecipeDetail = (obj: unknown): obj is RecipeDetail => {
return RecipeDetailSchema.safeParse(obj).success;
};
const validateRecipe = (data: unknown) => {
if (isRecipeDetail(data)) {
console.log(data.title);
}
};
❌ Bad:
const validateRecipe = (data: any) => {
if (data.title) {
console.log(data.title);
}
};
Why: Zod-based type guards provide both runtime validation and TypeScript type narrowing.
Common Mistakes
❌ Don't Use any
const processData = (data: any): any => {};
const processData = (data: unknown) => {
if (isValidData(data)) {
}
};
❌ Don't Disable Strict Mode
{ "strict": false }
{ "strict": true }
❌ Don't Assert Types Without Validation
const user = apiResponse as User;
const result = UserSchema.safeParse(apiResponse);
if (result.success) {
const user = result.data;
}
❌ Don't Overuse Enums
enum Color {
Red = 'RED',
Blue = 'BLUE',
}
export const COLOR = {
RED: 'RED',
BLUE: 'BLUE',
} as const;
export type Color = (typeof COLOR)[keyof typeof COLOR];
Testing
Runtime Zod Schema Testing
describe('RecipeDetailSchema', () => {
it('should validate valid recipe detail', () => {
const validRecipe = {
id: 'recipe-123',
title: 'Pasta Carbonara',
heroImageUrl: 'https://example.com/pasta.jpg',
savedDate: '2025-01-01T12:00:00Z',
ingredients: [
{ name: 'Pasta', measurement: '500g' },
],
};
const result = RecipeDetailSchema.safeParse(validRecipe);
expect(result.success).toBe(true);
});
it('should reject invalid URL', () => {
const invalidRecipe = {
id: 'recipe-123',
title: 'Pasta',
heroImageUrl: 'not-a-url',
savedDate: '2025-01-01T12:00:00Z',
ingredients: [],
};
const result = RecipeDetailSchema.safeParse(invalidRecipe);
expect(result.success).toBe(false);
});
});
Testing Type Guards
describe('isRecipeDetail', () => {
it('should return true for valid data', () => {
const validData = {
id: 'recipe-123',
title: 'Test Recipe',
heroImageUrl: 'https://example.com/image.jpg',
savedDate: '2025-01-01T00:00:00Z',
ingredients: [{ name: 'Test', measurement: '1 cup' }],
};
expect(isRecipeDetail(validData)).toBe(true);
});
it('should return false for null/undefined', () => {
expect(isRecipeDetail(null)).toBe(false);
expect(isRecipeDetail(undefined)).toBe(false);
});
});
Quick Reference
Type Decisions:
- Union types →
type
- Extendable objects →
interface
- Constants →
as const (not enums)
- Unknown data →
unknown (not any)
Validation:
- Define Zod schema first
- Infer TypeScript type with
z.infer<>
- Create type guards with
schema.safeParse()
- Create parse functions for detailed errors
Type Safety:
- Never use
any
- Enable strict mode
- Explicit function return types
- Optional chaining for null checks
import type for type-only imports
Additional Resources
For detailed examples and patterns, see: