Why does this RecursivePartial implementation not work with Dates?

Viewed 51

In this question on implementing a recursive partial in typescript, we get some answers that look good... except the latest answer points out they are all incomplete.

Let's take a closer look with examples:

These are three of the proposed solutions:

//The simple one
type SolutionA<T> = {
    [P in keyof T]?: SolutionA<T[P]>;
};

//The 'complete' one
type SolutionB<T> = {
  [P in keyof T]?:
    T[P] extends (infer U)[] ? SolutionB<U>[] :
    T[P] extends object ? SolutionB<T[P]> :
    T[P];
};

//The one that accounts for Date
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>;

And here is the example that shows A and B are incomplete:

type TT = { dateValue: Date }
const x1: SolutionA<TT> = { dateValue: "0" } // counterintuitively allowed
const x2: SolutionB<TT> = { dateValue: "0" } // counterintuitively allowed
const x3: SolutionC<TT> = { dateValue: "0" } // correctly disallowed by ts

Why does this happen? Is there a way to make a recursive partial without having to manually include every single exception like Maps, Sets, etc? What's the common thread between these 'exceptional' types? Should I worry about someone creating their own exception that has to be added to the list?

1 Answers

You would need to handle each native type (i.e. Date, Map, Set, etc.) individually, something like...

type NonAny = number | boolean | string | symbol | null;
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends NonAny[] // checks for nested any[]
    ? T[P]
    : T[P] extends ReadonlyArray<NonAny> // checks for nested ReadonlyArray<any>
    ? T[P]
    : T[P] extends Date // checks for Date
    ? T[P]
    : T[P] extends (infer U)[]
    ? DeepPartial<U>[]
    : T[P] extends ReadonlyArray<infer U>
    ? ReadonlyArray<DeepPartial<U>>
    : T[P] extends Set<infer V> // checks for Sets
    ? Set<DeepPartial<V>>
    : T[P] extends Map<infer K, infer V> // checks for Maps
    ? Map<K, DeepPartial<V>>
    : T[P] extends NonAny // checks for primative values
    ? T[P]
    : DeepPartial<T[P]>; // recurse for all non-array, non-date and non-primative values
};

Then your type enforcement would be correct. See TS playground.

type TT = { dateValue: Date }
const x1: DeepPartial<TT> = { dateValue: "0" } // correctly disallowed by ts

x1.dateValue?.getDate() // allowed method invocation

See related post How to implement TypeScript deep partial mapped type not breaking array properties

See TS playground troubleshooting Data as string condition here

Related