Hone logo
Hone
Problems

Implementing Advanced extends Utilities in TypeScript

TypeScript's conditional types and utility types allow for powerful meta-programming capabilities. This challenge focuses on implementing custom utility types that leverage extends to perform complex type transformations, similar to how built-in utilities like Exclude, Extract, and NonNullable work but with greater complexity and custom logic.

Problem Description

Your task is to implement several advanced TypeScript utility types that extend the functionality of conditional types. These utilities will be used to manipulate union types, filter properties, and transform object types based on specific conditions.

Key Requirements

  1. Implement FilterUnion<U, Condition>: This utility should take a union type U and a Condition type. It should return a new union type containing only the members of U that are assignable to Condition.
  2. Implement OmitByValue<T, V>: This utility should take an object type T and a value type V. It should return a new object type with all properties from T whose value type is assignable to V removed.
  3. Implement PickByValue<T, V>: This utility should take an object type T and a value type V. It should return a new object type with all properties from T whose value type is assignable to V picked.
  4. Implement TupleToUnion<T>: This utility should take a tuple type T and convert it into a union of its constituent element types.

Expected Behavior

  • The utilities should work with various primitive types, object types, union types, and intersection types.
  • They should correctly handle recursive types if they naturally arise from the structure.
  • The order of properties in the resulting object types is not significant.

Edge Cases

  • Empty union types.
  • Empty object types.
  • Types that are assignable to never or unknown.
  • Tuple types with mixed element types.

Examples

Example 1: FilterUnion<U, Condition>

// Input:
type MyUnion = string | number | boolean | null;
type StringOrNumber = string | number;

type Filtered = FilterUnion<MyUnion, StringOrNumber>;
// Expected Output: string | number

// Explanation:
// 'string' is assignable to 'string | number'.
// 'number' is assignable to 'string | number'.
// 'boolean' is NOT assignable to 'string | number'.
// 'null' is NOT assignable to 'string | number'.
// Therefore, the resulting union is 'string | number'.

Example 2: OmitByValue<T, V>

// Input:
interface Person {
  name: string;
  age: number;
  address: { street: string; city: string };
  isActive: boolean;
}

type Omitted = OmitByValue<Person, string | boolean>;
// Expected Output: { age: number; address: { street: string; city: string } }

// Explanation:
// 'name' has type 'string', which is assignable to 'string | boolean'. It's omitted.
// 'age' has type 'number', which is NOT assignable to 'string | boolean'. It's kept.
// 'address' has type '{ street: string; city: string }', which is NOT assignable to 'string | boolean'. It's kept.
// 'isActive' has type 'boolean', which is assignable to 'string | boolean'. It's omitted.

Example 3: PickByValue<T, V>

// Input:
interface Product {
  id: number;
  name: string;
  price: number;
  description: string;
  inStock: boolean;
}

type Picked = PickByValue<Product, number>;
// Expected Output: { id: number; price: number }

// Explanation:
// 'id' has type 'number', which is assignable to 'number'. It's picked.
// 'name' has type 'string', which is NOT assignable to 'number'. It's omitted.
// 'price' has type 'number', which is assignable to 'number'. It's picked.
// 'description' has type 'string', which is NOT assignable to 'number'. It's omitted.
// 'inStock' has type 'boolean', which is NOT assignable to 'number'. It's omitted.

Example 4: TupleToUnion<T>

// Input:
type MyTuple = [1, 'hello', true];
type MyTupleWithObject = [{ id: 1 }, { name: 'test' }];

type UnionFromTuple = TupleToUnion<MyTuple>;
// Expected Output: 1 | 'hello' | true

type UnionFromObjectTuple = TupleToUnion<MyTupleWithObject>;
// Expected Output: { id: 1 } | { name: 'test' }

// Explanation:
// For MyTuple, each element '1', 'hello', and 'true' becomes a member of the resulting union.
// For MyTupleWithObject, each object type within the tuple becomes a member of the resulting union.

Example 5: Edge Case - Empty Union/Object

// Input:
type EmptyUnion = never;
type FilteredEmpty = FilterUnion<EmptyUnion, string>;
// Expected Output: never

type EmptyObject = {};
type OmittedFromEmpty = OmitByValue<EmptyObject, string>;
// Expected Output: {}

Constraints

  • You must use only standard TypeScript features (generics, conditional types, mapped types, infer).
  • No external libraries are allowed.
  • The implementation should be efficient in terms of type checking time, though this is largely dictated by TypeScript's compiler.
  • Do not use brute-force approaches that involve creating large, synthetic types unnecessarily.

Notes

  • Remember how extends works in conditional types: T extends U ? X : Y. If T is assignable to U, the condition is true.
  • For object transformations like OmitByValue and PickByValue, you'll likely need to iterate over the keys of the object type. Mapped types ([K in keyof T]: ...) are your friends here.
  • Consider the distributive nature of conditional types when applied to union types.
  • The infer keyword can be very useful for extracting parts of types.
  • Think about how you can combine keyof, extends, and mapped types to achieve the desired object transformations.
  • For TupleToUnion, you might need to iterate through the tuple elements. A common pattern for this is to use a recursive type or leverage conditional types with infer.
Loading editor...
typescript