Exploring the topic of implementing a recursive partial in typescript, a question on Stack Overflow sparked some interesting discussions. The answers provided seemed promising, but upon further examination, the latest answer pointed out their incompleteness.
To delve deeper into this issue, let's consider three proposed solutions:
//Solution A
type SolutionA<T> = {
[P in keyof T]?: SolutionA<T[P]>;
};
//Solution B
type SolutionB<T> = {
[P in keyof T]?:
T[P] extends (infer U)[] ? SolutionB<U>[] :
T[P] extends object ? SolutionB<T[P]> :
T[P];
};
//Solution C
type SolutionC<T> = {
[P in keyof T]?:
T[P] extends Array<infer U> ? Array<Value<U>> : Value<T[P]>;
};
type AllowedPrimitives = boolean | string | number | Date /* add any types than should be considered as a value, say, DateTimeOffset */;
type Value<T> = T extends AllowedPrimitives ? T : SolutionC<T>;
An example is provided to demonstrate that Solutions A and B fall short:
type TT = { dateValue: Date }
const x1: SolutionA<TT> = { dateValue: "0" } // allowed unexpectedly
const x2: SolutionB<TT> = { dateValue: "0" } // allowed unexpectedly
const x3: SolutionC<TT> = { dateValue: "0" } // correctly disallowed by TypeScript
This raises questions about the necessity for manual inclusion of exceptions like Maps and Sets when creating a recursive partial. Are there common characteristics among these 'exceptional' types? Is there a concern about potential new exceptions arising that require adding to the list?