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

The technique for accessing nested key-value pairs in a JSON object within an Angular application

After receiving the response from the backend, I have retrieved a nested hash map structure where one hash map is nested within another: hmap.put(l,hmaps); //hmap within hmap When returning the response to the frontend, I am using the ResponseEntity meth ...

Avoid Inferring as a Union Type

I am currently working on implementing a compact type-safe coordinate management system in TypeScript. It revolves around defining the origin of the coordinate as a type parameter, with functions that only accept one specific origin type. Below is a short ...

Utilizing TypeScript generic types as a key for an object

function createRecord<T extends string>(key: T): Record<T, string> { return { [key]: 'asdf' }; } Encountering an issue: The type '{ [x: string]: string; }' is not matching with the expected 'Record<T, st ...

Insert an ellipsis within the ngFor iteration

I'm currently working with a table in which the td elements are filled with data structured like this: <td style="width:15%"> <span *ngFor="let org of rowData.organization; last as isLast"> {{org?.name}} ...

Troubleshooting issue: matTooltip malfunctioning in *ngFor loop after invoking Angular's change

The matTooltip in the component below is rendering correctly. The overlay and small bubble for the tooltip are rendered, but the text is missing (even though it's present in the HTML when inspecting in the browser) and it isn't positioned correct ...

Can you explain the distinction between employing 'from' and 'of' in switchMap?

Here is my TypeScript code utilizing RxJS: function getParam(val:any):Observable<any> { return from(val).pipe(delay(1000)) } of(1,2,3,4).pipe( switchMap(val => getParam(val)) ).subscribe(val => console.log(val)); ...

Using the spread operator in the console.log function is successful, but encountering issues when attempting to assign or return it in a

Currently facing an issue with a spread operator that's really getting on my nerves. Despite searching extensively, I haven't found a solution yet. Whenever I utilize console.log(...val), it displays the data flawlessly without any errors. Howev ...

Having trouble connecting my chosen color from the color picker

Currently, I am working on an angularJS typescript application where I am trying to retrieve a color from a color picker. While I am successfully obtaining the value from the color picker, I am facing difficulty in binding this color as a background to my ...

Implementing a video pause event trigger from a function in Angular2

Here is the content of my player.component.html: <video width="320" height="240" autoplay autobuffer [src]="videoSrc" (ended)="videoEnd()"> Your browser does not support the video tag. </video> <button (click)="pauseOrPlay()">pause/play ...

Typescript: Subscribed information mysteriously disappeared

[ Voting to avoid putting everything inside ngOnit because I need to reuse the API response and model array in multiple functions. Need a way to reuse without cluttering up ngOnInit. I could simply call subscribe repeatedly in each function to solve the p ...

How can I extract a value from an object that is readonly, using a formatted string as the key?

I encountered a situation where I have code resembling the following snippet. It involves an object called errorMessages and multiple fields. Each field corresponds to various error messages in the errorMessages object, but using a formatted string to retr ...

Retrieve unique elements from an array obtained from a web API using angular brackets

I've developed a web application using .NET Core 3.1 that interacts with a JSON API, returning data in the format shown below: [ { "partner": "Santander", "tradeDate": "2020-05-23T10:03:12", "isin": "DOL110", "type ...

Ways to incorporate forms.value .dirty into an if statement for an Angular reactive form

I'm a beginner with Angular and I'm working with reactive Angular forms. In my form, I have two password fields and I want to ensure that only one password is updated at a time. If someone tries to edit both Password1 and Password2 input fields s ...

Differences between RxJs Observable<string> and Observable<string[]>

I'm struggling to grasp the concept of RxJS Observables, even though I have utilized the observable pattern in various scenarios in my life. Below is a snippet of code that showcases my confusion: const observable: Observable<Response> = cr ...

Developing an Angular 2 class array

Incorporating Angular 2 and TypeScript, my goal is to construct an array of a specific class. import { ClassX } from '...'; public ListX: ClassX[]; Subsequently, once the list is established, I aim to append additional empty instances of C ...

The error message "The type 'DynamicModule' from Nest.js cannot be assigned to the type 'ForwardReference' within the nest-modules/mailer" was encountered during development

Recently, I decided to enhance my Nest.js application by integrating the MailerModule. I thought of using the helpful guide provided at this link: Acting on this idea, I went ahead and performed the following steps: To start with, I executed the command ...

How can I update a dropdown menu depending on the selection made in another dropdown using Angular

I am trying to dynamically change the options in one dropdown based on the selection made in another dropdown. ts.file Countries: Array<any> = [ { name: '1st of the month', states: [ {name: '16th of the month&apos ...

Tips for creating a TypeScript function that is based on another function, but with certain template parameters fixed

How can I easily modify a function's template parameter in TypeScript? const selectFromObj = <T, S>(obj: T, selector: (obj: T) => S): S => selector(obj) // some function from external library type SpecificType = {a: string, b: number} co ...

Troubleshooting `TypeError: document.createRange is not a function` error when testing material ui popper using react-testing-library

I am currently working with a material-ui TextField that triggers the opening of a Popper when focused. My challenge now is to test this particular interaction using react-testing-library. Component: import ClickAwayListener from '@material-ui/core/ ...

Encountering an undefined property error while using Array.filter in Angular 2

hello everyone, I am currently faced with an issue while working on a project that involves filtering JSON data. When using the developer tools in Chrome, it keeps showing me an error related to undefined property. chart: JsonChart[] = []; charts: JsonC ...