2

I want to write utility type that takes object as generic parameter and union of string and recursively make specifies properties optional. Sounds easy, however I encountered a problem I need help to solve. I wrote such utility type:

export type DeepPartial<Type, Property> = Type extends Array<infer ArrayType>
  ? Array<DeepPartial<ArrayType, Property>>
  : Type extends Record<string, unknown>
    ? {
    [Key in Extract<keyof Type, Property>]?: DeepPartial<Type[Key], Property>;
  } & {
    [Key in Exclude<keyof Type, Property>]: DeepPartial<Type[Key], Property>;
  }
    : Type;

It works very well with one exception. If passed type already has optional properties it requires such property to exist in created type (with possible value undefined), while they should not be required. Example:

type Test = {
  a: boolean,
  b: {
    a: 1,
    b: {
      c?: string;
    };
  },
  c?: string;
};

Variable defined below have invalid type (while it should not):

const d: DeepPartial<Test, 'a'> = {b: {b: {}}};

In order it work I need to explicity give object with optional property with undefined value:

const d: DeepPartial<Test, 'a'> = {b: {b: {c: undefined}}, c: undefined};

TS Playground link: https://www.typescriptlang.org/play?#code/C4TwDgpgBAKhDOwoF4oG8CwAoKUCGAXFAEYD2pANhHgHYA02uxRmOu+RAjA20y4+1wBjAPxFEAJwCWNAOYBuAbgC+itsp7CxUSTIXZV2bKEhQAIhAhgACngnApeCgB4YdKNYmkwUCAA9gCBoAE3gdYGk5AD4UWF8AoNCoACUIIVIJYOddOXcAVxoAaxpSAHcaKOwRdAEAbQBpKBkoAFEAiTwhYGdCiBBSADNYd09vKIBdbQsrW3tHFxgG8ZGvMCi1FSgAMhreKAammla-IQo84IgevsHhj1WJommbOwcnVyWVsY2oZShsIhgamw6RoiCgwUelmeczecEQ7gA5HgETFUGhmOgMWhlDi1EA

Furman
  • 2,017
  • 3
  • 25
  • 43
  • Does [this approach](https://tsplay.dev/WJx9DN) solve your problem? If yes, I'll write an answer explaining; If not, what am I missing? – wonderflame May 09 '23 at 12:46
  • Looks like it is working - thanks, you can post answer. However it is a bit convoluted solution I need a moment to process it. – Furman May 09 '23 at 17:35
  • Ok I get the idea - actually it was pretty simple (but clever) idea to apply conditional type checking Required>, I managed to simplify your solution a little bit, to be honest I don't understand why you are using union to intersection. – Furman May 09 '23 at 17:44
  • https://www.typescriptlang.org/play?#code/C4TwDgpgBAKhDOwoF4oG8CwAoKUCGAXFAEYD2pANhHgHYA02uxRmOu+RAjA20y4+1wBjAPxFEAJwCWNAOYBuAbgC+itsp7CxUSTIXZV2bKEhQAIhAhgACngnApeCgB4YdKNYmkwUCAA9gCBoAE3gdYGk5AD4UWF8AoNCoACUIIVIJYOddOXcAVxoAaxpSAHcaKOwRdAEAbQBpKBkoAFEAiTwhYGdCiBBSADNYd09vKIBdbQsrW3tHFxgG8ZGvMCi1FSgAMigACiVWvyEKPOCIHr7B4Y9VmP9AkLCZAYgJFIQke8SwnNkoEQOuGsUiEhVc7lSiDuCUe7wAjnkpBIIFlUulMs5IcB8kUSuUojEAbxBOgGk0aO9EOMiNMbHYHE5XEsVmNlFACID2Ggyc0sZMaZY6XNGYt6ssbqz2ZyaBAAG6vA4ASmwRBgamw6RoiCgwQFM3p81cH3cAHI8CaYqg0Mx0Da0MoHWogA – Furman May 09 '23 at 17:44
  • Please check [this playground](https://tsplay.dev/N95JVN). I have added a `d: string` side by side with `c?: string` and with your type in the result it became optional, however we would expect it to be required. That's why we need to treat the rest of the keys separately which will cause us a union, thus at the end we will have join those unions together with `UnionToIntersection`. I'm currently covering this in my answer post – wonderflame May 09 '23 at 17:47

1 Answers1

1

There might be a better approach for dealing with this, however, this is the best that I came up with:

Utility types

Prettify - is a utility to make types easy to read:

type Prettify<T> = T extends infer R
  ? {
      [K in keyof R]: R[K];
    }
  : never;

UnionToIntersection - turns union types to intersection. Reference:

type UnionToIntersection<U> = (
  U extends any ? (k: U) => void : never
) extends (k: infer I) => void
  ? I
  : never;

Solution

Your implementation is almost finished except for the last & part. In your implementation, you can't check for the ? identifier. Therefore we will change the logic of recursion. First, let's infer a type for the rest of the keys that are not in Property:

Exclude<keyof Type, Property> extends infer Rest extends string.

Then, we have to distribute the union members in Rest to treat the remaining keys separately; otherwise, If one of the properties in the Rest is optional and the other is not, in the end, both of them will be marked as optional, which is not expected. We will distribute the Rest with the following condition:

Rest extends Rest ? ... : never

If this condition is true the following code will be executed for each of the Rest union's members separately.

To check if some property is optional or not, we can pick the piece that we are testing and check if it extends its' required version, which can be done as follows:

Pick<Type, Rest> extends Required<Pick<Type, Rest>> ? true : false

If this condition is true then the property is required, otherwise, it is optional:

Pick<Type, Rest> extends Required<Pick<Type, Rest>>
    ? { [Key in Rest]: DeepPartial<Type[Key], Property> }
    : { [Key in Rest]?: DeepPartial<Type[Key], Property> }
: never

As we have distributed the Rest this whole piece will result in the union; however, they should be just one object; thus, we will use UnionToIntersection to convert it to a single object and for it to look pretty, we will wrap the whole DeepPartial in Prettify.

Full code:

type DeepPartial<Type, Property> = Prettify<
  Type extends Array<infer ArrayType>
    ? Array<DeepPartial<ArrayType, Property>>
    : Type extends Record<string, unknown>
    ? {
        [Key in Extract<keyof Type, Property>]?: DeepPartial<
          Type[Key],
          Property
        >;
      } & UnionToIntersection<
        Exclude<keyof Type, Property> extends infer Rest extends string
          ? Rest extends Rest
            ? Pick<Type, Rest> extends Required<Pick<Type, Rest>>
              ? { [Key in Rest]: DeepPartial<Type[Key], Property> }
              : { [Key in Rest]?: DeepPartial<Type[Key], Property> }
            : never
          : never
      >
    : Type
>;

playground

wonderflame
  • 6,759
  • 1
  • 1
  • 17