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
- Implement
FilterUnion<U, Condition>: This utility should take a union typeUand aConditiontype. It should return a new union type containing only the members ofUthat are assignable toCondition. - Implement
OmitByValue<T, V>: This utility should take an object typeTand a value typeV. It should return a new object type with all properties fromTwhose value type is assignable toVremoved. - Implement
PickByValue<T, V>: This utility should take an object typeTand a value typeV. It should return a new object type with all properties fromTwhose value type is assignable toVpicked. - Implement
TupleToUnion<T>: This utility should take a tuple typeTand 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
neverorunknown. - 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
extendsworks in conditional types:T extends U ? X : Y. IfTis assignable toU, the condition is true. - For object transformations like
OmitByValueandPickByValue, 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
inferkeyword 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 withinfer.