DEV Community

Alex Chen
Alex Chen

Posted on

TypeScript Tips That Make You More Productive

TypeScript Tips That Make You More Productive

Stop fighting TypeScript. Make it work for you.

1. Type vs Interface (When to Use Which)

// Use interface for object shapes
interface User {
  id: string;
  name: string;
  email: string;
  role: 'admin' | 'user' | 'moderator';
}

// Use type for unions, tuples, computed types
type ID = string | number;
type Status = 'pending' | 'active' | 'archived';
type Coordinates = [number, number]; // Tuple
type UserKeys = keyof User;          // 'id' | 'name' | 'email' | 'role'

// They can extend each other!
interface AdminUser extends User {
  permissions: string[];
  lastLogin: Date;
}

type EnhancedUser = User & {
  createdAt: Date;
  avatar?: string;
};
Enter fullscreen mode Exit fullscreen mode

2. Utility Types (Built-in Type Transformers)

interface Todo {
  id: number;
  title: string;
  description: string;
  completed: boolean;
  createdAt: Date;
}

// Partial — all fields optional
function updateTodo(id: number, fields: Partial<Todo>) {
  // Only update the provided fields
}
updateTodo(1, { completed: true }); // Only need to pass what's changing

// Required — make all fields required
type StrictTodo = Required<Todo>;

// Omit — remove specific keys
type TodoPreview = Omit<Todo, 'description' | 'createdAt'>;

// Pick — keep only specific keys
type TodoSummary = Pick<Todo, 'id' | 'title' | 'completed'>;

// Readonly — immutable version
const initialTodo: Readonly<Todo> = { ... };
initialTodo.completed = true; // Error! Cannot assign

// Record — key-value map type
const userRoles: Record<string, string> = {
  admin: 'full_access',
  editor: 'content_write',
  viewer: 'read_only',
};

// Extract — extract type from a complex type
type TodoTitle = Todo['title']; // string
Enter fullscreen mode Exit fullscreen mode

3. Generic Functions (Reusable Logic)

// Basic generic
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}

first([1, 2, 3]);     // number | undefined
first(['a', 'b']);    // string | undefined

// Generic with constraints
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

getProperty({ name: 'Alex', age: 30 }, 'name'); // string
getProperty({ name: 'Alex', age: 30 }, 'age');  // number
getProperty({ name: 'Alex', age: 30 }, 'xyz');   // Error! 'xyz' not a key

// Generic with multiple constraints
interface HasId { id: string; }
interface HasTimestamp { createdAt: Date; }

function logEntity<T extends HasId & HasTimestamp>(entity: T): void {
  console.log(`Entity ${entity.id} created at ${entity.createdAt}`);
}
Enter fullscreen mode Exit fullscreen mode

4. Discriminated Unions (Type-Safe State)

// The pattern that makes TypeScript shine:
type RequestState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: Error };

function handleState(state: RequestState<User>): string {
  switch (state.status) {
    case 'idle':
      return 'Not started';
    case 'loading':
      return 'Loading...';
    case 'success':
      return `Loaded: ${state.data.name}`; // TS knows data exists here!
    case 'error':
      return `Error: ${state.error.message}`; // TS knows error exists here!
    default:
      const _exhaustive: never = state; // Catch missing cases at compile time!
      return _exhaustive;
  }
}
Enter fullscreen mode Exit fullscreen mode

5. Type Guards (Narrowing Types)

// typeof guard
function printId(id: string | number) {
  if (typeof id === 'string') {
    console.log(id.toUpperCase()); // TS knows it's string here
  } else {
    console.log(id.toFixed(2));   // TS knows it's number here
  }
}

// instanceof guard
class Dog { bark() {} }
class Cat { meow() {} }
function speak(pet: Dog | Cat) {
  if (pet instanceof Dog) {
    pet.bark(); // TS knows it's Dog
  } else {
    pet.meow(); // TS knows it's Cat
  }
}

// Custom type guard (isX pattern)
interface ApiResponse<T> {
  success: true;
  data: T;
}
interface ApiError {
  success: false;
  error: { code: string; message: string };
}

type Result<T> = ApiResponse<T> | ApiError;

function isSuccess<T>(result: Result<T>): result is ApiResponse<T> {
  return result.success === true;
}

function handleResult(result: Result<User>) {
  if (isSuccess(result)) {
    console.log(result.data.name); // TS knows data is available!
  } else {
    console.log(result.error.message); // TS knows error is available!
  }
}
Enter fullscreen mode Exit fullscreen mode

6. satisfies Operator (Validate Without Changing Type)

// Before satisfies: `as const` makes everything readonly/literal
const config = {
  api: 'https://api.example.com',
  port: 3000,
  debug: false,
} as const;
// config.port is now number (literal 3000), not assignable to where number expected

// With satisfies: validates shape but keeps inferred type
const config = {
  api: 'https://api.example.com',
  port: 3000,
  debug: false,
} satisfies Record<string, string | number | boolean>;
// Validated! And port is still just `number`, not literal `3000`
Enter fullscreen mode Exit fullscreen mode

7. Template Literal Types

type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type APIRoute = `/api/${string}`;

type Endpoint = `${HTTPMethod} ${APIRoute}`;
// "GET /api/users" | "POST /api/login" etc.

// Practical: CSS property validation
type CSSProperty = `--${string}`;

// Event name patterns
type DOMEvent = `on${Capitalize<string>}`;
// "onClick" | "onChange" | "onSubmit"
Enter fullscreen mode Exit fullscreen mode

8. const Assertions and as const

// Regular object inference (types are widened)
const colors = ['red', 'green', 'blue'];
// Type: string[] (lost the specific values!)

// const assertion
const COLORS = ['red', 'green', 'blue'] as const;
// Type: readonly ['red', 'green', 'blue'] (values preserved!)

// Use in function parameters
function setColor(color: typeof COLORS[number]) {
  // color can only be 'red', 'green', or 'blue'
}
setColor('red');   // OK
setColor('yellow'); // Error!

// Object as const
const ROUTES = {
  home: '/',
  users: '/users',
  userProfile: '/users/:id',
} as const;
// Type: { readonly home: '/'; readonly users: '/users'; readonly userProfile: '/users/:id' }

type RouteKey = keyof ROUTES; // 'home' | 'users' | 'userProfile'
Enter fullscreen mode Exit fullscreen mode

9. Error Handling Pattern

// Wrap async functions to return [data, error] tuple instead of throwing
async function tryCatch<T>(promise: Promise<T>): Promise<[T?, Error?]> {
  try {
    const data = await promise;
    return [data, undefined];
  } catch (error) {
    return [undefined, error as Error];
  }
}

// Usage:
const [user, error] = await tryCatch(fetchUser(userId));
if (error) {
  console.error('Failed:', error);
  return;
}
console.log(user.name); // TS knows user is defined here
Enter fullscreen mode Exit fullscreen mode

10. tsconfig.json Essentials

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",

    "strict": true,                    // Enable all strict checks
    "noUncheckedIndexedAccess": true,  // array[i] could be undefined
    "noImplicitReturns": true,         // All paths must return value
    "noFallthroughCasesInSwitch": true,// Prevent switch fallthrough

    "skipLibCheck": true,              // Skip type checking .d.ts files
    "forceConsistentCasingInFileNames": true,

    "resolveJsonModule": true,
    "esModuleInterop": true,

    "declaration": true,               // Generate .d.ts files
    "declarationMap": true,
    "sourceMap": true,

    "outDir": "./dist",
    "rootDir": "./src",

    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@components/*": ["src/components/*"],
      "@utils/*": ["src/utils/*"]
    },

    "lib": ["ES2022", "DOM", "DOM.Iterable"]
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "test"]
}
Enter fullscreen mode Exit fullscreen mode

What's your favorite TypeScript tip? Share it below!

Follow @armorbreak for more developer content.

Top comments (0)