| name | typescript-patterns |
| description | Practical TypeScript patterns. Activated when working with type inference, utility types, generics, type guards, or type narrowing. |
TypeScript Patterns
Practical type authoring patterns
Type Inference
const name: string = 'John';
const items: string[] = ['a', 'b', 'c'];
const name = 'John';
const items = ['a', 'b', 'c'];
When to annotate: function parameters, complex return types, exported types
Utility Types
type UpdateUser = Partial<User>;
type UserPreview = Pick<User, 'id' | 'name'>;
type CreateUser = Omit<User, 'id' | 'createdAt'>;
type RequiredConfig = Required<Config>;
type UserMap = Record<string, User>;
type UpdateUser = Partial<User> & { id: string };
type CreateUser = Omit<User, 'id'> & { id?: string };
Narrowing
typeof, in, instanceof
function process(input: string | number) {
if (typeof input === 'string') {
return input.toUpperCase();
}
return input.toFixed(2);
}
function handleResponse(res: SuccessResponse | ErrorResponse) {
if ('error' in res) {
console.error(res.error);
return;
}
return res.data;
}
Discriminated Union (Recommended)
type Result =
| { status: 'success'; data: User }
| { status: 'error'; error: Error }
| { status: 'loading' };
function handleResult(result: Result) {
switch (result.status) {
case 'success':
return result.data;
case 'error':
throw result.error;
case 'loading':
return null;
}
}
Custom Type Guards
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
'name' in value
);
}
const data: unknown = await fetchData();
if (isUser(data)) {
console.log(data.name);
}
Generics
interface ApiResponse<T> {
data: T;
status: number;
}
function findById<T extends { id: string }>(items: T[], id: string): T | undefined {
return items.find(item => item.id === id);
}
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
keyExtractor: (item: T) => string;
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map(item => (
<li key={keyExtractor(item)}>{renderItem(item)}</li>
))}
</ul>
);
}
as const
const ROUTES = {
HOME: '/',
ABOUT: '/about',
USER: '/user',
} as const;
type Route = typeof ROUTES[keyof typeof ROUTES];
const STATUSES = ['pending', 'active', 'done'] as const;
type Status = typeof STATUSES[number];
DO NOT
function process(data: any) { ... }
function process(data: unknown) {
if (isValidData(data)) { ... }
}
const user = data as User;
if (isUser(data)) { const user = data; }
const name = user!.name!;
const name = user?.name ?? 'Unknown';
enum Status { Active, Inactive }
const Status = { Active: 'active', Inactive: 'inactive' } as const;
type Status = typeof Status[keyof typeof Status];
Practical Tips
type ButtonProps = React.ComponentProps<'button'> & {
variant?: 'primary' | 'secondary';
};
const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
setValue(e.target.value);
};
async function fetchUser(id: string): Promise<User> {
const response = await api.get(`/users/${id}`);
return response.data;
}
| Principle | Description |
|---|
| Types are docs | Express intent clearly, don't overcomplicate |
| Infer first | Only annotate when inference falls short |
| unknown > any | Always prefer unknown with guards |