CN

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:

Common Types
// 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:

Type vs Interface Usage
// 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:

Practical Utility Types
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:

Type Guards
// 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:

Generic Types
// 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:

React Types
// 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:

API Type Patterns
// 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:

Type Assertions
// 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:

Complete Example
// 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.

Want to read more articles like that?

Sign up to get notified when I publish more.

No spam. One click unsubscribe.

Read More on Fado Code Camp