TypeScript Types You'll Actually Use: A Practical Guide
Master the most useful TypeScript types with real-world examples. Learn utility types, type manipulation, and patterns for everyday TypeScript development.
TypeScript has hundreds of features, but you'll use these ones every day. Here's what actually matters.
Basic Types That Matter
Start with these - they cover 90% of use cases:
// Objects
interface User {
id: number;
name: string;
email: string;
preferences?: UserPreferences; // Optional
roles: Array<string>; // or string[]
}
// Function types
type Handler = (event: MouseEvent) => void;
type AsyncHandler = (data: unknown) => Promise<void>;
// Union types for options
type Status = 'pending' | 'success' | 'error';
type ID = string | number;
Type vs Interface
Use interface for objects and type for everything else:
// Use interface for objects
interface Product {
id: number;
name: string;
price: number;
}
// Use type for unions, functions, primitives
type Price = number;
type Callback = () => void;
type Status = 'active' | 'inactive';
// Both work but interface is clearer for objects
interface User {
name: string;
}
interface User { // Can be extended
age: number;
}
Utility Types You'll Actually Use
These built-in types solve common problems:
interface User {
id: number;
name: string;
email: string;
password: string;
}
// Remove sensitive fields
type SafeUser = Omit<User, 'password'>;
// Make everything optional for updates
type UserUpdate = Partial<User>;
// Make everything required
type RequiredUser = Required<User>;
// Pick only what you need
type LoginCredentials = Pick<User, 'email' | 'password'>;
// Get type of array item
const items = ['a', 'b', 'c'];
type Item = typeof items[number]; // string
Type Guards for Runtime Checks
TypeScript can't check types at runtime. Use these patterns:
// Simple type guard
function isString(value: unknown): value is string {
return typeof value === 'string';
}
// Object type guard
interface User { name: string; }
interface Admin { name: string; role: string; }
function isAdmin(user: User | Admin): user is Admin {
return 'role' in user;
}
// Usage
function handleUser(user: User | Admin) {
if (isAdmin(user)) {
console.log(user.role); // TypeScript knows it exists
}
}
// API response type guard
interface ApiResponse<T> {
data: T;
error?: string;
}
function isSuccessResponse<T>(
response: ApiResponse<T>
): response is ApiResponse<T> & { data: T } {
return !response.error;
}
Generic Type Patterns
Make your types reusable:
// API response wrapper
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
// Async state manager
interface AsyncState<T> {
data: T | null;
loading: boolean;
error: Error | null;
}
// Function with generic constraint
function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
const result = {} as Pick<T, K>;
keys.forEach(key => {
result[key] = obj[key];
});
return result;
}
// Usage
const user = { name: 'John', age: 30, email: 'john@example.com' };
const basicInfo = pick(user, ['name', 'age']);
React Component Types
If you're using React, you'll need these:
// Props interface
interface ButtonProps {
label: string;
onClick: () => void;
disabled?: boolean;
children?: React.ReactNode;
}
// Component with props
const Button: React.FC<ButtonProps> = ({
label,
onClick,
disabled = false,
children
}) => {
return (
<button onClick={onClick} disabled={disabled}>
{label}
{children}
</button>
);
};
// Event handlers
interface FormProps {
onSubmit: (e: React.FormEvent) => void;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}
// Ref types
interface InputProps {
inputRef: React.RefObject<HTMLInputElement>;
}
API Types
Common patterns for API interactions:
// Base pagination type
interface PaginatedResponse<T> {
items: T[];
total: number;
page: number;
pageSize: number;
hasMore: boolean;
}
// Request params
interface SearchParams {
query: string;
page?: number;
limit?: number;
sort?: 'asc' | 'desc';
}
// API error
interface ApiError {
code: string;
message: string;
details?: Record<string, unknown>;
}
// Combine them
async function searchApi<T>(
params: SearchParams
): Promise<PaginatedResponse<T> | ApiError> {
// Implementation
}
Type Assertions
Sometimes you need to override TypeScript:
// When you know better than TypeScript
const userId = (event.target as HTMLElement).dataset.userId;
// Force type when TypeScript is wrong
interface User { name: string }
const user = JSON.parse('{"name":"John"}') as User;
// Double assertion for any -> specific type
const input = {} as any as HTMLInputElement;
Real World Example
Here's how these patterns work together:
// Types
interface User {
id: number;
name: string;
email: string;
}
interface ApiResponse<T> {
data: T;
error?: string;
}
type AsyncState<T> = {
data: T | null;
loading: boolean;
error: string | null;
};
// Type guard
function isSuccessResponse<T>(
response: ApiResponse<T>
): response is ApiResponse<T> & { data: T } {
return !response.error;
}
// React hook with types
function useApi<T>() {
const [state, setState] = useState<AsyncState<T>>({
data: null,
loading: false,
error: null
});
const fetch = async (url: string) => {
setState(prev => ({ ...prev, loading: true }));
try {
const response = await fetchApi<T>(url);
if (isSuccessResponse(response)) {
setState({
data: response.data,
loading: false,
error: null
});
} else {
throw new Error(response.error);
}
} catch (error) {
setState({
data: null,
loading: false,
error: error instanceof Error ? error.message : 'Unknown error'
});
}
};
return { ...state, fetch };
}
// Usage
function UserList() {
const { data, loading, error, fetch } = useApi<User[]>();
useEffect(() => {
fetch('/api/users');
}, []);
if (loading) return <div>Loading...</div>;
if (error) return <div>{error}</div>;
if (!data) return null;
return (
<ul>
{data.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}
Remember: TypeScript is about being practical, not perfect. These patterns give you 95% of the safety with 5% of the complexity.