Valerii Pohorzhelskyi

Back to blog

Advanced TypeScript Types That Changed How I Code

TypeScriptJavaScriptType Safety

After three years of writing TypeScript, I've discovered that the real power isn't in the basic type annotations - it's in the advanced type system features that let you encode complex business logic at the type level.

Conditional Types

Conditional types let you create types that change based on other types. Here's a practical example from our API client:

type ApiResponse<T> = T extends { error: string }
  ? { success: false; error: string }
  : { success: true; data: T };

async function fetchUser(id: string): Promise<ApiResponse<User>> {
  try {
    const user = await api.get(`/users/${id}`);
    return { success: true, data: user };
  } catch (error) {
    return { success: false, error: error.message };
  }
}

// TypeScript knows the exact shape!
const result = await fetchUser('123');
if (result.success) {
  console.log(result.data.name); // ✅ Type-safe access
} else {
  console.log(result.error); // ✅ Error is string
}

Template Literal Types

One of my favorite features introduced in TypeScript 4.1 is template literal types. They're perfect for creating type-safe event systems:

type EventName = 'click' | 'focus' | 'blur';
type EventHandler<T extends EventName> = `on${Capitalize<T>}`;

type Handlers = {
  [K in EventName as EventHandler<K>]: (event: Event) => void;
};

// Results in:
// {
//   onClick: (event: Event) => void;
//   onFocus: (event: Event) => void;
//   onBlur: (event: Event) => void;
// }

Mapped Types for Form Validation

I use mapped types extensively for form validation. Here's a pattern that ensures every form field has corresponding validation:

type FormData = {
  email: string;
  password: string;
  age: number;
};

type Validator<T> = (value: T) => string | null;

type FormValidators<T> = {
  [K in keyof T]: Validator<T[K]>;
};

const validators: FormValidators<FormData> = {
  email: (value) => {
    return value.includes('@') ? null : 'Invalid email';
  },
  password: (value) => {
    return value.length >= 8 ? null : 'Password too short';
  },
  age: (value) => {
    return value >= 18 ? null : 'Must be 18 or older';
  }
  // TypeScript will error if we forget a field!
};

Recursive Types for Tree Structures

Before TypeScript 4.1, recursive types were limited. Now they're incredibly powerful:

type TreeNode<T> = {
  value: T;
  children?: TreeNode<T>[];
};

function findInTree<T>(
  node: TreeNode<T>,
  predicate: (value: T) => boolean
): T | null {
  if (predicate(node.value)) {
    return node.value;
  }

  if (node.children) {
    for (const child of node.children) {
      const found = findInTree(child, predicate);
      if (found) return found;
    }
  }

  return null;
}

The satisfies Operator

TypeScript 4.9 introduced satisfies, which solves the problem of wanting both type checking AND type inference:

type Color = {
  r: number;
  g: number;
  b: number;
};

const palette = {
  red: { r: 255, g: 0, b: 0 },
  green: { r: 0, g: 255, b: 0 },
  blue: { r: 0, g: 0, b: 255 }
} satisfies Record<string, Color>;

// TypeScript knows exact keys!
palette.red; // ✅
palette.yellow; // ❌ Property 'yellow' does not exist

// And we still have autocomplete!

Real-World Impact

These advanced types have eliminated entire classes of runtime errors in our codebase:

  • 55% reduction in type-related bugs caught in code review
  • Zero runtime type errors in the last 6 months
  • Improved DX with better autocomplete and inline documentation

The initial learning curve is steep, but the payoff in code quality and developer confidence is absolutely worth it.