Hone logo
Hone
Problems

Mastering Conditional Types in TypeScript

TypeScript's conditional types are a powerful tool for creating flexible and dynamic type definitions. They allow you to define types that change their structure based on whether a certain condition is met. This challenge will test your understanding and ability to implement custom conditional type helpers.

Problem Description

Your task is to create a set of TypeScript conditional type helpers that manipulate types based on specific conditions. You will implement three distinct conditional type helpers:

  1. IsEqual<T, U>: This type should resolve to true if T and U are strictly equal types, and false otherwise.
  2. ExcludeByCondition<T, Condition>: This type should take a union type T and exclude any member of T that satisfies the Condition. The Condition itself will be a generic type parameter that takes a member of T and returns a boolean-like type (e.g., true or false).
  3. ExtractByCondition<T, Condition>: This type should take a union type T and extract only those members of T that satisfy the Condition. Similar to ExcludeByCondition, Condition is a generic type parameter.

Key Requirements:

  • All implementations must use TypeScript's conditional types (extends ? :).
  • The helpers should be generic and work with various primitive and complex types.
  • The Condition type parameter in ExcludeByCondition and ExtractByCondition is designed to be a type that can be applied to each member of the union T.

Expected Behavior:

  • IsEqual<string, string> should resolve to true.
  • IsEqual<string, number> should resolve to false.
  • ExcludeByCondition<string | number | boolean, string> should resolve to number | boolean.
  • ExcludeByCondition<string | number | boolean, number> should resolve to string | boolean.
  • ExtractByCondition<string | number | boolean, string> should resolve to string.
  • ExtractByCondition<string | number | boolean, number> should resolve to number.

Edge Cases:

  • Consider how IsEqual handles different kinds of types, including unions, intersections, and object types.
  • Ensure ExcludeByCondition and ExtractByCondition correctly process unions of various types.
  • Think about what happens if the Condition type parameter is a simple type rather than a conditional type that checks a property. (For the purpose of this challenge, assume Condition is designed to be used in a conditional check like T extends Condition).

Examples

Example 1: IsEqual

type Result1 = IsEqual<string, string>; // Expected: true
type Result2 = IsEqual<string, number>; // Expected: false
type Result3 = IsEqual<{ a: number }, { a: number }>; // Expected: true
type Result4 = IsEqual<{ a: number }, { b: number }>; // Expected: false
type Result5 = IsEqual<string | number, number | string>; // Expected: true

Explanation: These examples demonstrate how IsEqual should correctly identify identical types and differentiate between distinct ones.

Example 2: ExcludeByCondition and ExtractByCondition

type MyUnion = string | number | { name: string } | boolean;

// Assuming a simple type check for condition (e.g., checking if it's a string)
type IsString<T> = T extends string ? true : false;

type ExcludedStrings = ExcludeByCondition<MyUnion, string>; // Expected: number | { name: string } | boolean
type ExtractedStrings = ExtractByCondition<MyUnion, string>; // Expected: string

// Example with a more complex condition: extracting only objects with a 'name' property
type HasName<T> = T extends { name: any } ? true : false;

type ExtractedNamedObjects = ExtractByCondition<MyUnion, { name: any }>; // Expected: { name: string }
type ExcludedNamedObjects = ExcludeByCondition<MyUnion, { name: any }>; // Expected: string | number | boolean

Explanation: These examples show how ExcludeByCondition removes types that match a condition, while ExtractByCondition keeps only those that match. The Condition type parameter is a placeholder for a type that can be checked against the members of the union. For the purpose of these examples, we're simulating this check directly with types like string and { name: any }.

Example 3: Edge Cases for IsEqual

type Obj1 = { x: number; y: string };
type Obj2 = { y: string; x: number }; // Same properties, different order
type Obj3 = { x: number; y: string; z: boolean };

type Result6 = IsEqual<Obj1, Obj2>; // Expected: true
type Result7 = IsEqual<Obj1, Obj3>; // Expected: false
type Result8 = IsEqual<any, string>; // Expected: false (any is special)
type Result9 = IsEqual<unknown, string>; // Expected: false

Explanation: This example highlights how IsEqual should treat object types with the same properties as equal, regardless of order. It also touches upon the distinct nature of any and unknown.

Constraints

  • The solution must be written entirely in TypeScript.
  • You should aim for a clean and readable implementation of the conditional types.
  • No external libraries or type utilities are allowed; implement these from scratch.
  • The IsEqual type should correctly handle primitive types, object types, union types, and intersection types.
  • For ExcludeByCondition and ExtractByCondition, the Condition type parameter will be used in a way that allows TypeScript to infer its applicability (e.g., T extends Condition).

Notes

  • Remember that T extends U ? X : Y is the core syntax for conditional types.
  • When working with unions in conditional types, TypeScript often distributes the conditional type over the union. You might need to use techniques to prevent this distribution if you intend to compare the entire union as a single unit (though for IsEqual it's usually beneficial, and for Exclude/Extract it's handled implicitly by iterating over the union).
  • Consider how never and unknown behave in type comparisons.
  • For IsEqual, a common pattern involves checking if T extends U AND U extends T.
Loading editor...
typescript