DiegoVallejo

The TypeScript Wizard Cheat Sheet: Beyond `any`

1. The infer Keyword (Extracting Inner Types)

Stop importing types from dependencies if you can extract them dynamically.

// Problem: You need the return type of a function, but it's not exported.
// Solution: Use `infer` to unwrap the structure.

type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

// Advanced: Unwrapping Promises recursively (Awaited<T> implementation)
type Unwrap<T> = T extends Promise<infer U> ? Unwrap<U> : T;

async function getData() { return { id: 1, name: 'Admin' }; }
type UserData = Unwrap<ReturnType<typeof getData>>; 
// Result: { id: number, name: string }

2. Conditional Control Flow

Types can have logic. Use this to create APIs that adapt based on input.

// If T is 'json', return object. If 'text', return string.
type ResponseType<T extends 'json' | 'text'> = 
  T extends 'json' ? object : string;

declare function fetchAPI<T extends 'json' | 'text'>(
  mode: T
): Promise<ResponseType<T>>;

const data = await fetchAPI('json'); // Typed as object
const text = await fetchAPI('text'); // Typed as string

3. Template Literal Types (Dynamic Strings)

Validate string formats at compile time. Essential for event buses and CSS frameworks.

type Entity = 'User' | 'Post';
type EventType = 'create' | 'delete';

// result: 'User:create' | 'User:delete' | 'Post:create' | 'Post:delete'
type AppEvent = `${Entity}:${EventType}`; 

// CSS Padding Utility
type Space = 'sm' | 'md' | 'lg';
type Padding = `p-${Space}` | `px-${Space}` | `py-${Space}`;

function style(p: Padding) { /* ... */ }
style('p-md'); // OK
style('p-xl'); // Error

4. Key Remapping & Mapped Types

Transform existing types into new structures automatically.

type User = {
  id: number;
  name: string;
  email: string;
};

// Create a type that generates "getters" for all fields
// Key Remapping: `as` clause allows renaming keys
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
};

type UserGetters = Getters<User>;
// Result:
// {
//   getId: () => number;
//   getName: () => string;
//   getEmail: () => string;
// }

5. Branded Primitives (Nominal Typing)

TypeScript is structurally typed. string equals string. This is dangerous for IDs. Use "Branding" to create distinct types that compile away to primitives at runtime (O(0) cost).

declare const __brand: unique symbol;
type Brand<B, T = string> = T & { [__brand]: B };

type UserId = Brand<'UserId'>;
type OrderId = Brand<'OrderId'>;

const fetchOrder = (id: OrderId) => { /*...*/ };

const uid = 'u-123' as UserId;
const oid = 'o-456' as OrderId;

fetchOrder(oid); // OK
fetchOrder(uid); // Error: Type 'UserId' is not assignable to type 'OrderId'.

6. Type Predicates (is)

Don't cast (as). Assert checks to narrow types securely.

interface Fish { swim: () => void }
interface Bird { fly: () => void }

// The return type `pet is Fish` tells the compiler 
// that if this returns true, the variable IS a Fish within that scope.
function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

const pet = getPet();
if (isFish(pet)) {
  pet.swim(); // Compiler knows `swim` exists here
}

7. The never Type (Exhaustiveness Checking)

Use never to ensure all cases in a switch statement are handled. If you add a new type to a union and forget to handle it, the build fails.

type Shape = 'circle' | 'square' | 'triangle';

function getArea(s: Shape) {
  switch (s) {
    case 'circle': return Math.PI;
    case 'square': return 1;
    case 'triangle': return 0.5;
    default:
      // This line will error if 'pentagon' is added to Shape
      const _exhaustiveCheck: never = s;
      return _exhaustiveCheck;
  }
}

8. Deep Partial (Utility)

Standard Partial<T> only works one level deep. Use recursion for config objects.

type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

type Config = {
  db: {
    host: string;
    port: number;
  }
};

const update: DeepPartial<Config> = {
  db: {
    // Allows omitting 'host' while keeping strictly typed 'port'
    port: 5432 
  }
};

9. Const Assertions (as const)

Freeze literals to their specific values, not their primitives.

// Without `as const`: type is { method: string, routes: string[] }
const config = {
  method: 'GET',
  routes: ['/home', '/admin']
};

// With `as const`: type is { readonly method: "GET"; readonly routes: readonly ["/home", "/admin"]; }
const strictConfig = {
  method: 'GET',
  routes: ['/home', '/admin']
} as const;

// Useful for Redux action creators or enforcing immutable constants

10. The satisfies Operator (Validation without Widening)

Stop using : Type annotation for configuration objects. It widens types and loses literal precision. Use satisfies.

// BAD: Widens 'timeout' to 'number', losing the literal 500
const config: Record<string, string | number> = {
  host: 'localhost',
  timeout: 500,
}; 
// config.host is string | number (Requires runtime check)

// GOOD: Enforces contract, but keeps specific types
const strictConfig = {
  host: 'localhost',
  timeout: 500,
} satisfies Record<string, string | number>;

strictConfig.host.toUpperCase(); // OK (Compiler knows it's a string)
strictConfig.timeout.toFixed();  // OK (Compiler knows it's a number)

11. Recursive Path Accessors (Dot Notation)

Essential for type-safe ORMs, Form libraries, and I18n keys.

type Paths<T> = T extends object 
  ? { [K in keyof T]: `${Exclude<K, symbol>}${"" | `.${Paths<T[K]>}`}` }[keyof T]
  : never;

interface User {
  profile: {
    name: string;
    address: {
      city: string;
    };
  };
}

// Result: "profile" | "profile.name" | "profile.address" | "profile.address.city"
type UserPaths = Paths<User>;

function get<T, P extends Paths<T>>(obj: T, path: P) { /*...*/ }

12. Union to Intersection (The Black Magic)

The most notorious type in the TS ecosystem. It converts A | B to A & B. Relies on contravariance of function arguments.

// Step 1: Distribute union into functions: (x: A) => void | (x: B) => void
// Step 2: Infer the intersection of arguments
type UnionToIntersection<U> = 
  (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;

type U = { a: string } | { b: number };
type I = UnionToIntersection<U>; 
// Result: { a: string } & { b: number }

13. Tuple Manipulation (Head & Tail)

TS 4.0+ allows spread syntax in tuple types. Use this for recursive array processing.

type Head<T extends any[]> = T extends [infer H, ...any[]] ? H : never;
type Tail<T extends any[]> = T extends [any, ...infer R] ? R : never;

type Arr = [1, 2, 3];
type H = Head<Arr>; // 1
type T = Tail<Arr>; // [2, 3]

// Usage: Type-safe "shift" operation
declare function shift<T extends any[]>(arr: T): [Head<T>, Tail<T>];

14. String Manipulation (Recursive Splitting)

Turning a string literal into a Tuple. Useful for parsing CSV headers or URL paths at compile time.

type Split<S extends string, D extends string> = 
  string extends S ? string[] : // Stop infinite loop on generic string
  S extends `${infer T}${D}${infer U}` ? [T, ...Split<U, D>] : [S];

type Path = "users/123/posts";
type Segments = Split<Path, "/">; 
// Result: ["users", "123", "posts"]

15. Mutable & Readonly Remapping

Remove readonly modifiers from existing types to allow mutation in utilities (use with caution).

type Mutable<T> = {
  -readonly [P in keyof T]: T[P]; // The "-" operator removes the modifier
};

interface Frozen {
  readonly id: number;
  readonly tags: string[];
}

type Editable = Mutable<Frozen>;
// { id: number; tags: string[]; }

16. Discriminated Unions with Exhaustiveness

The pattern for robust reducers and event handlers. The kind property acts as the discriminator.

type NetworkState = 
  | { state: 'loading' }
  | { state: 'success'; data: string }
  | { state: 'error'; code: number };

function logState(s: NetworkState) {
  if (s.state === 'success') {
    // TS knows `data` exists here
    console.log(s.data); 
  } else if (s.state === 'error') {
    // TS knows `code` exists here
    console.error(s.code);
  }
}

17. Variadic Tuple Types (Function Composition)

Typing a pipe or compose function that takes an indefinite number of arguments.

// Concatenate two tuples
type Concat<T extends unknown[], U extends unknown[]> = [...T, ...U];

type A = [1, 2];
type B = [3, 4];
type C = Concat<A, B>; // [1, 2, 3, 4]

// Example: Middleware Pipeline
type Middleware<T> = (input: T) => T;
declare function pipeline<T>(...funcs: Middleware<T>[]): T;

The Law of Conservation of Complexity

You cannot destroy complexity; you can only displace it. In software engineering, you have two choices for where this complexity lives:

  1. At Compile Time: Expressed through "Wizard" types, generics, and strict constraints.
  2. At Runtime: Expressed through extensive unit tests, defensive programming, and production bugs.

The techniques above are not "code golf." They are architectural decisions to shift the burden of validation from the CPU (runtime) to the Compiler.

The goal is not to write clever code. The goal is to make invalid states unrepresentable. If you have to write a unit test to check if a variable is a string or a number, your type system has failed you.

Compile strictly, sleep soundly.