Implement a Generic Split Type in TypeScript
In TypeScript, we often need to manipulate types to create more specific or generalized versions. This challenge focuses on creating a utility type that can "split" a given type into two parts based on a discriminant property. This is particularly useful for handling discriminated unions where you might want to isolate specific variants of a union.
Problem Description
Your task is to implement a generic TypeScript utility type called Split<T, K>. This type should take two generic arguments:
T: The type from which to split. This is expected to be a union of objects.K: The key of the discriminant property. This key will be used to determine how to split the union.
The Split type should return a tuple where:
- The first element of the tuple represents all members of the union
Tthat do not have the specific value of the discriminant propertyK. - The second element of the tuple represents all members of the union
Tthat do have the specific value of the discriminant propertyK.
Essentially, you are filtering the union based on the presence of a specific property value.
Key Requirements:
- The
Splittype must be generic and acceptT(a union of objects) andK(a string literal representing the discriminant key) as type parameters. - It should return a tuple
[TypeWithoutK, TypeWithK]. TypeWithoutKshould be a union of all members ofTthat do not possess the propertyK.TypeWithKshould be a union of all members ofTthat do possess the propertyK.
Expected Behavior:
When Split<T, K> is applied, it should analyze each member of the union T. If a member has the property K, it belongs to the second part of the tuple. If it does not have the property K, it belongs to the first part.
Edge Cases:
- What happens if
Tis not a union of objects? (While not explicitly tested in examples, consider the behavior.) - What if
Krefers to a property that doesn't exist on any of the union members? - What if a union member has the property
K, but its value isundefined? (For this challenge, considerundefinedas a value, so ifKisundefined, it should be included inTypeWithK).
Examples
Example 1:
type Shape =
| { type: "circle"; radius: number }
| { type: "square"; sideLength: number }
| { type: "triangle"; base: number; height: number };
type NonCircles = Split<Shape, "type">[0];
// Expected: { type: "square"; sideLength: number } | { type: "triangle"; base: number; height: number }
type Circles = Split<Shape, "type">[1];
// Expected: { type: "circle"; radius: number }
Explanation:
We are splitting the Shape union based on the type property. The Split<Shape, "type"> type should conceptually separate the shapes where type is not "circle" and those where type is "circle". The first element of the tuple contains non-circle shapes, and the second contains the circle shape.
Example 2:
type Event =
| { name: "click"; x: number; y: number }
| { name: "keydown"; key: string }
| { name: "scroll"; delta: number };
type NonKeyEvents = Split<Event, "key">[0];
// Expected: { name: "click"; x: number; y: number } | { name: "scroll"; delta: number }
type KeyEvents = Split<Event, "key">[1];
// Expected: { name: "keydown"; key: string }
Explanation:
Here, we split the Event union based on the presence of the key property. Events that don't have a key property go into the first part of the tuple, and the event that does have a key property goes into the second.
Example 3: (Edge Case - Discriminant property not present)
type DataPoint =
| { id: 1; value: string }
| { id: 2; timestamp: Date };
type WithoutTimestamp = Split<DataPoint, "timestamp">[0];
// Expected: { id: 1; value: string }
type WithTimestamp = Split<DataPoint, "timestamp">[1];
// Expected: { id: 2; timestamp: Date }
Explanation:
This example demonstrates splitting based on a property that is not present on all members of the union. DataPoint members without the timestamp property are in the first part, and the member with it is in the second.
Constraints
- The type
Tis assumed to be a union of object types. - The type
Kmust be a string literal type representing the key to discriminate by. - Your solution should use only standard TypeScript features (no external libraries).
- The solution should be efficient in terms of type computation.
Notes
Consider how you can use conditional types and mapped types to iterate over the union T and check for the presence of the property K. Think about how to construct the two resulting unions within a tuple. You might find the keyof operator and type inference useful. Remember that in TypeScript, keyof T returns a union of keys of an object T. You are checking for the presence of a specific key K on each member of the union T.