Typescript's Patch<T> type enforces strictness within the codebase

There have been instances where I needed to 'patch' an object T using Object.assign().

For instance, when propagating changes you might modify a stateful object that other code references (common in reactive programming like MobX or Vue).

It's crucial to ensure that the object still conforms to shape T at the end to prevent runtime errors as other code relies on it.

Up until now, this process has been manual, but I am curious about how the Typescript compiler can assist. My theory is that defining a Patch<T> type and a

function applyPatch<T, P extends Patch<T>>(current: T, patch: P)
could guarantee that the resulting object remains a valid T regardless of its original form by overwriting properties as necessary.

The illustration linked below view typescript playground demonstrates incorrect derivation of a Patch, leading to objects that are no longer T when combined with Object.assign, yet the compiler incorrectly perceives them as such which may result in runtime errors. In the code snippet, no objects are forcibly converted, highlighting a potential TypeScript soundness flaw.

If anyone can propose a definition for Patch<T> and applyPatch() that would trigger compilation errors for unsound patch attempts, it would be beneficial.

The goal is for the compiler to mandate correct properties in the patch - explicitly overwriting any properties requiring alignment.

type DataLoader<Ok, Error = unknown> =
  | ({ loading: boolean } & {
      data?: never;
      errors?: never;
    })
  | {
      loading: false;
      data?: never;
      errors: Error[];
    }
  | {
      loading: false;
      data: Ok;
      errors?: never;
    };

/** Identifying the 4 strictly-allowed variants of DataLoader<Foo> */

interface Foo {
  foo: "bar";
}

const createInitialValue = (): DataLoader<Foo> => ({ loading: false });

const createLoadingValue = (): DataLoader<Foo> => ({ loading: true });

const createSuccessValue = (): DataLoader<Foo> => ({
  loading: false,
  data: {
    foo: "bar",
  },
});

const createFailureValue = (): DataLoader<Foo> => ({
  loading: false,
  errors: [new Error("Went wrong")],
});

/** Attempting to define a safe patch routine */

/** Suboptimal patch definition for demonstration purposes */
type Patch<T> = Partial<T>;

function applyPatch<T, P extends Patch<T>>(current: T, patch: P) {
  return Object.assign(current, patch);
}

/** Highlighting the shortcomings */

// These examples showcase inappropriate setting of `loading` while leaving data or errors intact
// The patch should have forced `data` and `errors` values to be set to `undefined`  
// Accepted due to Object.assign limitations and loose DataLoaderPatch<T> constraints
const keptDataByMistake: DataLoader<Foo> = applyPatch(
  createSuccessValue(),
  {
    loading: true,
  }
);
const keptErrorsByMistake: DataLoader<Foo> = applyPatch(
  createFailureValue(),
  {
    loading: true,
  }
);

// Here, the loading value remains `true`, incompatible with data and errors
// The patch should have enforced setting the loading value to false 
// Accepted due to Object.assign limitations and loose DataLoaderPatch<T> constraints
const successButStillLoadingMistake: DataLoader<Foo> = applyPatch(
  createLoadingValue(),
  {
    data: { foo: "bar" },
  }
);
const failureButStillLoadingMistake: DataLoader<Foo> = applyPatch(
  createLoadingValue(),
  {
    errors: [new Error("Went wrong")],
  }
);

/** Demonstrating creation of type-invalid DataLoader<Foo> without compiler errors using the patching procedure */
for (const loader of [
  keptErrorsByMistake,
  keptDataByMistake,
  successButStillLoadingMistake,
  failureButStillLoadingMistake,
]) {
  console.log(JSON.stringify(loader));
}

Answer №1

After much consideration, I have devised a workaround that appears to be the most viable solution at this time, unless someone can propose something superior.

Regrettably, it falls short of achieving the expected SafePatch<T> type (a union type that ensures Object.assign(someT, safePatch) still produces a T). Such a feature would greatly improve auto-completion capabilities.

Nevertheless, by implementing explicit typing to track the merging process in Object.assign(), we are able to generate informative compiler errors (assuming familiarity with potential patching issues).

The explicitly defined type as

type Merged<Target, Source> = Omit<Target, keyof Source> & Source;
and using
return Object.assign(orig, patch) as Merged<Orig, Patch>;
provides sufficient type information to warn if an object that is not a T is created.

Below, there is a comparison between unsafePatch (basic Object.assign) and safePatch (typed Object.assign), which you can experiment with on this platform.

https://i.stack.imgur.com/SJuRJ.png

The encountered error message is displayed below, making it fairly traceable...

https://i.stack.imgur.com/ZqgLR.png

You can find the comprehensive demonstration below.

function unsafePatch<Orig, Patch>(orig: Orig, patch: Patch) {
  return Object.assign(orig, patch);
}

function safePatch<Orig, Patch>(orig: Orig, patch: Patch) {
  return Object.assign(orig, patch) as Merged<Orig, Patch>;
}

const loader: DataLoader<string> = createSuccessValue();

{
  // Unsafe version
  const badlyPatched: DataLoader<string> = unsafePatch(loader, {
    loading: true,
  });
  const wellPatched: DataLoader<string> = unsafePatch(loader, {
    loading: true,
    data: undefined,
    errors: undefined,
  });
}

{
  // Safe version
  const badlyPatched: DataLoader<string> = safePatch(loader, {
    loading: true,
  });
  const wellPatched: DataLoader<string> = safePatch(loader, {
    loading: true,
    data: undefined,
    errors: undefined,
  });
}
...</answer1>
<exanswer1><div class="answer" i="73599208" l="4.0" c="1662282065" a="Y2Vmbg==" ai="2257198">
<p>I've found an adequate workaround which might be the best answer, unless someone has a better idea.</p>
<p>Sadly it doesn't achieve the ideal of a <code>SafePatch<T> type (a union type guaranteeing that Object.assign(someT, safePatch) is also a T). This would be great as it would facilitate auto-completion.

However, introducing explicit typing that tracks the merge taking place in Object.assign() is enough to achieve fairly meaningful compiler errors (assuming you know how patching can fail).

The explicit type is

type Merged<Target, Source> = Omit<Target, keyof Source> & Source;
then
  return Object.assign(orig, patch) as Merged<Orig, Patch>;
returns sufficient type information to know that you may have made an object that isn't a T.

Compare the unsafePatch (simple Object.assign) with the safePatch (typed Object.assign) shown below (which you can explore at this playground).

https://i.stack.imgur.com/SJuRJ.png

The error appears like this, which is just about traceable...

https://i.stack.imgur.com/ZqgLR.png

The full demonstration is below.

function unsafePatch<Orig, Patch>(orig: Orig, patch: Patch) {
  return Object.assign(orig, patch);
}

function safePatch<Orig, Patch>(orig: Orig, patch: Patch) {
  return Object.assign(orig, patch) as Merged<Orig, Patch>;
}

const loader: DataLoader<string> = createSuccessValue();

{
  // unsafe version
  const badlyPatched: DataLoader<string> = unsafePatch(loader, {
    loading: true,
  });
  const wellPatched: DataLoader<string> = unsafePatch(loader, {
    loading: true,
    data: undefined,
    errors: undefined,
  });
}

{
  // safe version
  const badlyPatched: DataLoader<string> = safePatch(loader, {
    loading: true,
  });
  const wellPatched: DataLoader<string> = safePatch(loader, {
    loading: true,
    data: undefined,
    errors: undefined,
  });
}

function createInitialValue() {
  return { loading: false } as const;
}

function createLoadingValue() {
  return { loading: false } as const;
}

function createSuccessValue() {
  return {
    loading: false,
    data: "bar",
  } as const;
}

function createFailureValue() {
  return {
    loading: false,
    errors: [new Error("Went wrong")],
  } as const;
}

type Merged<Target, Source> = Omit<Target, keyof Source> & Source;

type DataLoader<Ok, Error = unknown> =
  // loading true or false, but has nothing yet
  | ({ loading: boolean } & {
      data?: never;
      errors?: never;
    })
  // retrieval failed - loading===false, has errors
  | {
      loading: false;
      data?: never;
      errors: Error[];
    }
  // retrieval succeeded - loading===false, has data
  | {
      loading: false;
      data: Ok;
      errors?: never;
    };

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

Having difficulty forming queries correctly using TypeScript, React, and GraphQL

Apologies for the potentially naive question, but I am new to working with GraphQL and React. I am attempting to create a component that contains a GraphQL query and incoming props. The props consist of a query that should be passed into the GraphQL query. ...

What steps should I follow to utilize a JavaScript dependency following an NPM installation?

After successfully installing Fuse.js using npm, I am having trouble using the dependency in my JavaScript code. The website instructions suggest adding the following code to make it work: var books = [{ 'ISBN': 'A', 'title&ap ...

Displayed even when data is present, the PrimeNg empty message persists

I have set up a PrimeNg table to display data with an empty message template like this: <ng-template pTemplate="emptymessage"> <tr> <td> No records found </td> </tr> </ng-template> ...

An error occurred while trying to load the configuration "next/core-web-vitals" for extension

If you're embarking on a new project using NextJs and TypeScript, chances are you may encounter the following error: Failed to load config "next/core-web-vitals" to extend from. Wondering how to resolve this issue? ...

The "npx prisma db seed" command encountered an issue: Exit code 1 error occurred during the execution of the command: ts-node --compiler-options {"module":"CommonJS"} prisma/seed.ts

this is a sample package.json file when I try to execute the command "npx prisma db seed", I encounter the following error: An error occurred while running the seed command: Error: Command failed with exit code 1: ts-node --compiler-options {&qu ...

PlayWright - Extracting the text from the <dd> element within a <div> container

Here is the structure I am working with: <div class="aClassName"> <dl> <dt>Employee Name</dt> <dd data-testid="employee1">Sam</dd> </dl> </div> I am attempting to retrie ...

Angular recognizing string-array type as a string input is not correct

I am encountering a challenge with an Angular CLI component that involves working with an array of strings called "searchResult": export class ParentComponent implements OnInit { mockArray: string[] = []; searchString: string = ''; searchR ...

Tips for creating Material UI elements using React and Typescript

I am looking to extend a component from the Material UI library by creating a custom component that encapsulates specific styles, such as for a modal wrapper. However, I feel that my current solution involving the props interface may not be ideal. Is the ...

Dynamically pass a template to a child component

How can I dynamically load content on my page based on the active navigation point? export class Sub_navigation_item { constructor( public title: string, public templateName: string ) {} } I have a navigation item with an ID from an ...

Determine the specific data types of the component properties in React Storybook using TypeScript

Currently, I am putting together a component in the storybook and this is how it appears: import React, { useCallback } from 'react'; import { ButtonProps } from './types'; const Button = (props: ButtonProps) => { // Extract the nec ...

What is the best way to use an Observable to interrogate a fork/join operation?

I have a forkjoin set up to check for the presence of a person in two different data stores. If the person is not found in either store, I want to perform a delete action which should return true if successful, and false otherwise. However, my current impl ...

"Error in Visual Studio: Identical global identifier found in Typescript code

I'm in the process of setting up a visual studio solution using angular 2. Initially, I'm creating the basic program outlined in this tutorial: https://angular.io/docs/ts/latest/guide/setup.html These are the three TS files that have been genera ...

tsconfig is overlooking the specified "paths" in my Vue project configuration

Despite seeing this issue multiple times, I am facing a problem with my "paths" object not working as expected. Originally, it was set up like this: "paths": { "@/*": ["src/*"] }, I made updates to it and now it looks like ...

Setting various colors for different plots within a single chart: A step-by-step guide

I'm currently tackling a project that requires me to showcase two different plots on the same chart, one being a "SPLINE" and the other a "COLUMN". My aim is to assign distinct background colors to each of these plots. Please note that I am referring ...

The deployment of the remix is unsuccessful in Vercel, even though it functions perfectly during development. The error message states that 'AbortController' is not

I'm new to React and could use some assistance with a deployment issue on Vercel. Any ideas on why this is failing? I haven't explicitly used AbortController anywhere, so I'm suspecting it might be related to one of the installed packages? ...

Enhanced Autocomplete Feature with Select All Option in MUI

Currently, I am utilizing Material UI (5) and the Autocomplete component with the option for multiselect enabled. In addition, I am implementing the "checkbox" customization as per the MUI documentation. To enhance this further, I am attempting to incorpor ...

How can I retrieve the `checked` state of an input in Vue 3 using Typescript?

In my current project, I am using the latest version of Vue (Vue 3) and TypeScript 4.4. I am facing an issue where I need to retrieve the value of a checkbox without resorting to (event.target as any).checked. Are there any alternative methods in Vue tha ...

What is the reason behind Flow's reluctance to infer the function type from its return value?

I was anticipating the code to undergo type checking within Flow just like it does within TypeScript: var onClick : (() => void) | (() => boolean); onClick = () => { return true; } However, I encountered this error instead: 4: onClick = () => ...

Unable to bind to ngModel as it returned as "undefined" in Angular 8

Whenever I bind a property to ngModel, it consistently returns undefined <div> <input type="radio" name="input-alumni" id="input-alumni-2" value="true" [(ngModel) ...

Utilizing two DTOs for a single controller in NestJS

I'm having trouble retrieving and transforming different types of dtos from the body. My goal is to extract and transform firstDto if it's incoming, or convert secondDto if that's what's being received. However, my current code isn&apos ...