Master conditional types for advanced type transformations.

Complex Conditional Logic

type IsNever<T> = [T] extends [never] ? true : false;
type IsAny<T> = 0 extends 1 & T ? true : false;
type IsUnknown<T> = IsAny<T> extends true
  ? false
  : unknown extends T
  ? true
  : false;

Type Equality

type Equals<X, Y> = (<T>() => T extends X ? 1 : 2) extends <
  T
>() => T extends Y ? 1 : 2
  ? true
  : false;

type Test1 = Equals<string, string>;  // true
type Test2 = Equals<string, number>;  // false

Extract Function Types

type FunctionPropertyNames<T> = {
  [K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];

type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>;

interface User {
  name: string;
  greet(): void;
  age: number;
  sayHello(): string;
}

type UserFunctions = FunctionProperties<User>;
// { greet(): void; sayHello(): string; }

Deep Partial

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

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

type PartialUser = DeepPartial<User>;
// {
//   name?: string;
//   address?: {
//     street?: string;
//     city?: string;
//   };
// }

Required Keys

type RequiredKeys<T> = {
  [K in keyof T]-?: {} extends Pick<T, K> ? never : K;
}[keyof T];

interface User {
  name: string;
  age?: number;
  email?: string;
}

type Required = RequiredKeys<User>; // "name"

Optional Keys

type OptionalKeys<T> = {
  [K in keyof T]-?: {} extends Pick<T, K> ? K : never;
}[keyof T];

interface User {
  name: string;
  age?: number;
  email?: string;
}

type Optional = OptionalKeys<User>; // "age" | "email"

Exclude Null and Undefined

type NonNullable<T> = T extends null | undefined ? never : T;

type T0 = NonNullable<string | number | undefined>; // string | number
type T1 = NonNullable<string[] | null | undefined>; // string[]

Extract Promise Type

type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;

type T0 = Awaited<Promise<string>>;           // string
type T1 = Awaited<Promise<Promise<number>>>;  // number

Union to Intersection

type UnionToIntersection<U> = (
  U extends any ? (x: U) => void : never
) extends (x: infer I) => void
  ? I
  : never;

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

Exercises

Exercise 1: Deep Required

// Create a type 'DeepRequired' that:
// - Makes all properties required recursively
// - Works with nested objects

Exercise 2: Type Equality

// Create a type 'NotEqual' that:
// - Returns true if types are not equal
// - Returns false if types are equal

Exercise 3: Extract Nested Types

// Create a type that extracts:
// - All function return types from an object
// - As a union type

Exercise 4: Conditional Mapping

// Create a type that:
// - Maps over object properties
// - Converts functions to their return types
// - Leaves other properties unchanged

Exercise 5: Complex Conditional

// Create a type 'Flatten' that:
// - Flattens nested object types
// - Example: { a: { b: string } } => { a_b: string }

Best Practices

  1. Understand distribution - Conditional types distribute over unions
  2. Use infer effectively - Extract types from complex structures
  3. Test your types - Ensure they work as expected
  4. Document complex logic - Add comments explaining the transformation
  5. Combine with mapped types - Powerful type transformations

Common Mistakes

  1. Not preventing distribution when needed
// Distributes
type T = ToArray<string | number>; // string[] | number[]

// Prevent with brackets
type T = ToArrayNonDist<string | number>; // (string | number)[]
  1. Wrong infer placement
// ❌ Wrong
type Bad<T> = T extends Array<any> ? infer U : T;

// ✅ Correct
type Good<T> = T extends Array<infer U> ? U : T;
  1. Circular conditional types
// ❌ Might cause issues
type A<T> = T extends string ? B<T> : never;
type B<T> = T extends string ? A<T> : never;

Next Steps

Learn about Mapped Types (Advanced).