Creating a specialized Typescript deep partial type for specific attributes

I have a challenge in writing a utility type that takes an object as a generic parameter and a union of strings to recursively make specific properties optional. While it may sound simple, I encountered an issue that I need assistance with. Here is the utility type I tried:

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;

This works well, except for one scenario. If the passed type already has optional properties, it requires those properties to exist in the created type (with a possible value of undefined), when they should actually not be required. For example:

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

The variable defined below has an invalid type (when it shouldn't):

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

In order for it to work correctly, I need to explicitly provide an object with optional properties set to undefined:

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

You can view this on TS Playground here: TS Playground Link

Answer №1

There may be alternative approaches to address this issue, but the following is the solution that I have come up with:

Handy Utilities

Prettify - serves as a utility for enhancing type readability:

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

UnionToIntersection - transforms union types into intersection. Reference:

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

Solution

Your implementation is nearly complete except for the final & part. In your current setup, it's not possible to check for the ? identifier. Thus, we will need to adjust the recursion logic. Firstly, let's deduce a type for the remaining keys not present in Property:

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

Next, we must distribute the individual members of the union within Rest to handle the remaining keys individually. If this is not done, properties could become unexpectedly optional if some are optional and others are not. The distribution of Rest should follow this condition:

Rest extends Rest ? ... : never

If the condition holds true, the code snippet will execute for each member of the Rest union separately.

To determine if a property is optional or required, we can compare the part being tested against its required version by utilizing the following method:

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

If the condition is met, then the property is considered 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

By distributing the Rest, all these pieces will combine into a union result; however, they should ideally form a single object. Utilizing UnionToIntersection, we can merge them into one coherent object and wrap the entire DeepPartial in Prettify for better presentation.

Complete 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

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

What is the best way to assign JSON data to a Class variable within Angular?

In my code, I have a class called Projects export class Projects { project_id: number; project_name: string; category_id: number; project_type: string; start_date: Date; completion_date: Date; working_status: string; project_info: string; area: string; add ...

"Exploring the world of Typescript declarations and the CommonJS module

I'm encountering confusion while creating declaration files (d.ts). For instance, I have developed an NPM package called "a" with a single CommonJS module index.ts: export interface IPoint { x: number; y: number; } export default function s ...

Attempting to populate an array with .map that commences with a designated number in Angular using Typescript

Currently, I am attempting to populate an array with a series of numbers, with the requirement that the array begins with a certain value and ends with another. My attempt at this task involved the code snippet below: pageArray = Array(finalPageValue).fil ...

A guide to successfully transferring data array values from a Parent Component to a Child Component using APIs in Angular

To transfer values of this.bookingInfo = bookings.responseObj.txnValues; array from the parent component to the bookingInfo array in my child component and then insert that data into the chartNameChartTTV.data = []; array in the child component. Here, divN ...

What is causing the transpiler to not be triggered by the code change?

My current project involves using a TypeScript backend for a Dialogflow application with fulfillment. I initially used a preconfigured project template and didn't delve into the details. I work in VS Code and never explicitly build my code. Instead, ...

Trouble with Angular 7: Form field not displaying touched status

I am encountering an issue while trying to input data into a form, as it is not registering the touched status. Consequently, an error always occurs when attempting to send a message back to the user. In my TypeScript file, I am utilizing FormBuilder to c ...

Creating trendy designs with styled components: A guide to styling functional components as children within styled parent components

I am looking to enhance the style of a FC styled element as a child inside another styled element. Check out the sandbox example here const ColorTextContainer = styled.div` font-weight: bold; ${RedBackgroundDiv} { color: white; } `; This resul ...

Transmit information between components through a form

Is there a way to transfer data from one component to another in Angular? I have two components set up and I am currently using a selector to display the HTML content in the first component. Now, I need to figure out how to send the data entered in a form ...

Guide to incorporating external code in InversifyJS without direct control

I'm wondering if it's feasible to add classes that are not editable. Inversify seems to rely heavily on annotations and decorators, but I'm curious if there is an alternative method. ...

Error encountered in Angular 7.2.0: Attempting to assign a value of type 'string' to a variable of type 'RunGuardsAndResolvers' is not allowed

Encountering an issue with Angular compiler-cli v.7.2.0: Error message: Types of property 'runGuardsAndResolvers' are incompatible. Type 'string' is not assignable to type 'RunGuardsAndResolvers' This error occurs when try ...

Dealing with an unspecified parameter can be tricky - here's how you

Currently, I am in the process of developing an angular application. In this project, there is a specific scenario that needs to be handled where a parameter is undefined. Here's a snippet of my code: myImage() { console.log('test') ...

The comparison of Booleans in Typescript sometimes produces inaccurate results

There is a strange issue I encountered in one of my classes involving a readonly boolean property. Whenever I try to check this property, the results are not as expected. Here is an example of the code: // vorgang is a reference to the class, isEK is the ...

Issue with the code: Only arrays and iterable objects are permitted in Angular 7

Trying to display some JSON data, but encountering the following error: Error Message: Error trying to diff 'Leanne Graham'. Only arrays and iterables are allowed Below is the code snippet: The Data {id: 1, name: "Leanne Graham"} app.compone ...

enhancing the types of parameters in a function declaration without relying on generics

My goal is to improve developer experience (DX) by expanding the types for parameters in a function signature. I want the tooltip when hovering over the following function to provide more detailed information: type Props = { a: number; }; const func = ( ...

What is the best way to reload a React/TypeScript page after submitting a POST request?

I am working on a custom plugin for Backstage that interacts with Argo CD via API calls. To retrieve application information, I make a GET request to the following endpoint: https://argocd.acme.com/api/v1/applications/${app-name} If the synchronizati ...

Display an image in an Angular application using a secure URL

I am trying to return an image using a blob request in Angular and display it in the HTML. I have implemented the following code: <img [src]="ImageUrl"/> This is the TypeScript code I am using: private src$ = new BehaviorSubject(this.Url); data ...

React Component not displaying properly when used inside a map iteration

I am currently working on rendering multiple components using the .map method on an array with specific content. Although I can't find any errors in the console, the component is not appearing in the DOM as expected. I attempted to set subHeader to nu ...

The expected function is being executed, yet none of the inner functions are invoked

Currently, I am working on unit tests for an Angular application using Jasmine and Karma. One particular unit test involves opening a modal, changing values in a form, and saving them. Everything goes smoothly until it reaches the promise inside the open() ...

DotLottie file loading issues

While using dotlottie/react-player, webpack 4, and react 16, I encountered module parse failed errors during compilation. "@dotlottie/react-player": "^1.6.5" "webpack": "^4.44.2", "react": "16.14.0&qu ...

What strategies can I implement to streamline the use of these functions instead of creating a separate one for each textfield

Currently, I am learning how to use spfx with SPO (SharePoint Online). In my form, there are multiple text fields that need to be handled. I have two class components named A and B. Whenever a textfield in component B is typed into, a function sends the in ...