TypeScript Tips Every Developer Should Know

Practical TypeScript tips and tricks to make your code more robust, readable, and easier to maintain.

Jean-Pierre Broeders

Freelance DevOps Engineer

February 18, 20264 min. read

TypeScript Tips Every Developer Should Know

TypeScript has become the default in many projects — and for good reason. But the language has a lot more to offer than just string and number. In this article I share concrete tips that will make your day-to-day TypeScript work much more enjoyable.

1. Use satisfies Instead of Type Assertions

Since TypeScript 4.9 you have the satisfies operator. It gives you the best of both worlds: type-checking AND automatic type inference.

const config = {
  port: 3000,
  host: "localhost",
  debug: true,
} satisfies Record<string, string | number | boolean>;

// TypeScript now knows config.port is a number — not string | number | boolean
console.log(config.port.toFixed(0)); // ✅ works

With a plain type annotation (: Record<string, ...>) you'd see config.port as a union type and lose the specific inference.

2. Template Literal Types for Safe String Patterns

You can now strongly type string patterns:

type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type ApiRoute = `/api/${string}`;
type Endpoint = `${HttpMethod} ${ApiRoute}`;

function callApi(endpoint: Endpoint): Promise<Response> {
  const [method, url] = endpoint.split(" ", 2);
  return fetch(url, { method });
}

callApi("GET /api/users");       // ✅
callApi("PATCH /api/users");     // ❌ Type error
callApi("GET users");            // ❌ Type error

This catches mistakes at compile-time that would otherwise only surface in production.

3. Discriminated Unions: Better Error Handling

Stop using any as the return type for functions that can fail. Use a discriminated union instead:

type Result<T> =
  | { success: true; data: T }
  | { success: false; error: string };

async function fetchUser(id: string): Promise<Result<User>> {
  try {
    const user = await db.users.findById(id);
    if (!user) return { success: false, error: "User not found" };
    return { success: true, data: user };
  } catch (e) {
    return { success: false, error: "Database error" };
  }
}

// Usage:
const result = await fetchUser("123");
if (result.success) {
  console.log(result.data.name); // TypeScript knows data exists here
} else {
  console.error(result.error);   // And that error exists here
}

4. const Assertions for Immutable Data

Want an object or array to be treated as fully readonly with literal types?

const STATUSES = ["pending", "active", "archived"] as const;
type Status = typeof STATUSES[number]; // "pending" | "active" | "archived"

const DEFAULT_CONFIG = {
  retries: 3,
  timeout: 5000,
  strategy: "exponential",
} as const;

Without as const, Status would just be string. With as const you get exact literal types — and you can never accidentally mutate the array.

5. Smart Use of Utility Types

TypeScript has a rich standard library of utility types. Use them:

interface User {
  id: string;
  name: string;
  email: string;
  role: "admin" | "user";
  createdAt: Date;
}

// Only certain fields required when creating
type CreateUserDto = Pick<User, "name" | "email" | "role">;

// Everything optional for updates
type UpdateUserDto = Partial<Pick<User, "name" | "email">>;

// Everything readonly for view-only contexts
type UserView = Readonly<User>;

// Without sensitive fields
type PublicUser = Omit<User, "email" | "role">;

This saves you a lot of duplicate type definitions.

6. Generics with Constraints

Generics only become truly powerful when you add constraints:

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

const user = { name: "JP", age: 30, active: true };
const name = getProperty(user, "name");   // type: string ✅
const age  = getProperty(user, "age");    // type: number ✅
getProperty(user, "missing");             // ❌ compile error

7. Type Guards for Runtime Safety

TypeScript types only exist at compile-time. When processing data from the outside world (APIs, JSON), you need runtime checks:

interface ApiUser {
  id: string;
  name: string;
  email: string;
}

function isApiUser(data: unknown): data is ApiUser {
  return (
    typeof data === "object" &&
    data !== null &&
    typeof (data as ApiUser).id === "string" &&
    typeof (data as ApiUser).name === "string" &&
    typeof (data as ApiUser).email === "string"
  );
}

const raw = await response.json();
if (isApiUser(raw)) {
  console.log(raw.name); // ✅ TypeScript trusts it now
}

Conclusion

TypeScript is more than a type-checker on top of JavaScript. With satisfies, discriminated unions, template literal types, and smart utility types you write code that contains fewer bugs and is easier to read and refactor. Start with one tip you aren't using yet and build from there.

Good TypeScript isn't an art — it's a habit.

Want to stay updated?

Subscribe to my newsletter or get in touch for freelance projects.

Get in Touch