Creating a versatile transformer function for TypeScript subtypes without relying on type assertions

Currently, I am diving into the world of functional programming using TypeScript for a personal project. My focus lies on harnessing the power of higher-order functions and the pipe function to craft expressive transformers. While experimenting with these concepts, I encountered issues with TypeScript typings, particularly when attempting to design a generic function that produces a transformer from an object type to a subtype of that object.

Let me outline the scenario:

type Cat = {
  whiskers: number;
}

type FancyCat = Cat & {
  tophatSize: number;
}

type PartyCat = Cat & {
  balloons: number;
}

My objective is to devise a function that dynamically generates a transformer to modify any property on any object, ultimately transforming the object into a subtype of itself by adding properties.

While I can achieve this outcome with a hardcoded approach like so:

const createFancyCatTransformer = (props: { tophatSize: number }) => (cat: Cat) => {
  return {
    ...cat,
    ...props
  }
}

const cat = {
  whiskers: 50
}

const fancyCat = pipe(cat, createFancyCatTransformer({ tophatSize: 5 }));

I aspire to generalize this process. Instead of utilizing createFancyCatTransformer, my aim is to implement a versatile function such as

createTransformerToSubtype<Cat, FancyCat>({ tophatSize: 5 })
.

Despite my attempts, I have struggled to make this concept work effectively. As a simplified alternative, I endeavored to develop a transformer capable of extending Cat to one of its subtypes, only to encounter stumbling blocks.

Here's my endeavor at crafting a generic transformer for Cat:

const createCatExtender =
  <E extends Cat>(extension: Omit<E, keyof Cat>) =>
  (originalObject: Cat): E => {
    return { ...originalObject, ...extension };
  };

// Usage
createCatExtender<PartyCat>({ balloons: 7 });

Unfortunately, I faced a TypeScript error:

Type '{ whiskers: number; } & Omit<E, "whiskers">' is not assignable to type 'E'.
  '{ whiskers: number; } & Omit<E, "whiskers">' is assignable to the constraint of type 'E', but 'E' could be instantiated with a different subtype of constraint 'Cat'

Absolutely no room for type assertions or similar workarounds are acceptable. Although the solution seems apparent if I assert "as E" in the return statement, my goal remains to circumvent that route.

Is there a way to devise a generic rendition of this function devoid of TypeScript errors? How can I fashion a transformer that universally updates a property on any object type?

Answer №1

To create a flexible transformation function, you can utilize two generics (B for base and E for extension) along with the utility function RemoveProperties:

type Cat = {
  whiskers: number;
}

type FancyCat = Cat & {
  tophatSize: number;
}

type PartyCat = Cat & {
  balloons: number;
}

type RemoveProperties<T, U> = Omit<T, keyof U>;

const createTransformer = <
B extends {},
E extends B>(extension: RemoveProperties<E, B>
) => {
  return (base: B) => {
    return { ...base, ...extension } as E;
    // using as E is optional but it enhances readability compared to B & RemoveProperties<E, B>
  }
}

This function can be used like so:

const fancyCatTransformer = createTransformer<Cat, FancyCat>({ tophatSize: 3 });
const partyCatTransformer = createTransformer<Cat, PartyCat>({ balloons: 7 });

const baseCat = { whiskers: 3 };

const fancyCat = fancyCatTransformer(baseCat);

console.log(
  fancyCat.tophatSize, // Valid
  fancyCat.whiskers, // Valid
  fancyCat.balloons // Invalid
);

const partyCat = partyCatTransformer(baseCat);

console.log(
  partyCat.tophatSize, // Invalid
  partyCat.whiskers, // Valid
  partyCat.balloons // Valid
);

The createTransformer function is designed to work with different types such as Cat.

Extra Tip

If desired, by substituting extends B with extends {}, you can eliminate the need for as E

const createTransformer = <
B extends {},
E extends {}>(extension: RemoveProperties<E, B> // Change extends B here
) => {
  return (base: B) => {
    return { ...base, ...extension }; // Remove as E here
  }
}

This modification will allow you to perform transformations between different types like this:

const fancyPartyCatTransformer = createTransformer<
  FancyCat,
  PartyCat
>({ balloons: 7 });

const fancyPartyCat = fancyPartyCatTransformer(fancyCat);

console.log(
  fancyPartyCat.tophatSize, // Valid
  fancyPartyCat.whiskers, // Valid
  fancyPartyCat.balloons // Valid
);

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

Triggering multiple updates within a single method in Angular will cause the effect or computed function to only be triggered

Upon entering the realm of signals, our team stumbled upon a peculiar issue. Picture a scenario where a component has an effect on a signal which is a public member. In the constructor of the component, certain logic is executed to update the signal value ...

Exploring Vue.js 3: Validating props with a custom type definition

I am currently working on a Vue.js 3 and Typescript single page application project. The issue I am facing involves a view and a single file component. In the People.vue component, data is fetched from the backend and displayed in multiple instances of th ...

How can I reduce unnecessary spacing in a primeNg Dropdown (p-dropdown) filter within an Angular 5 application?

In my Angular 5 project, I have implemented PrimeNG dropdown (p-dropdown) and encountered an issue. When I try to filter the dropdown data by adding spaces before and after the search term, it displays a No Results Found message. How can I fix this problem ...

Error with Background component in Next.js-TypeScript when trying to change color on mouseover because Window is not defined

Within my Background component, there is a function that includes an SVG which changes color upon mouseover. While this functionality works fine, I encountered an error when hosting the project using Vercel - "ReferenceError: window is not defined." This i ...

Generate an Observable<boolean> from a service function once two subscriptions have successfully completed

I am working on setting up a simple method to compare the current username with a profile's username in an Angular service. It is necessary for the profile username and user's username to be resolved before they can be compared. How can I create ...

Could you provide an explanation of the styled() function in TypeScript?

const Flex = styled(Stack, { shouldForwardProp: (prop) => calcShouldForwardProp(prop), })<LayoutProps>(({ center, autoWidth, autoFlex, theme }) => ({ })); This syntax is a bit confusing to me. I understand the functionality of the code, b ...

Keep the code running in JavaScript even in the presence of TypeScript errors

While working with create-react-app and typescript, I prefer for javascript execution not to be stopped if a typescript error is detected. Instead, I would like to receive a warning in the console without interrupting the UI. Is it feasible to adjust the ...

"Resulting in 'undefined' due to an asynchronous function call

Encountering an issue with async method implementation. In my authServices, there is a loginWithCredential function which is asynchronous: async loginWithCredential(username, password){ var data = {username: username, password: password}; api.pos ...

Angular - Implementing filter functionality for an array of objects based on multiple dropdown selections

I am currently working on filtering an array of objects based on four fields from a form. These four fields can be combined for more specific filtering. The four fields consist of two dropdowns with multiple selection options and two text boxes. Upon cli ...

Is it possible to combine various SVG icons into a single component?

I am currently able to code SVGs in React-Native using typescript. This allows me to call them as individual react native components. Below is an example of my current capability: <View> <BackArrow color ="red" wid ...

One inventive method for tagging various strings within Typescript Template Literals

As TypeScript 4.1 was released, many developers have been exploring ways to strictly type strings with predetermined patterns. I recently found a good solution for date strings, but now I'm tackling the challenge of Hex color codes. The simple approa ...

Leverage the child interface as a property interface containing a generic interface

I'm facing an issue while trying to incorporate typings in React. The concept is centered around having an enum (EBreakpoint) that correlates with each device we support. A proxy wrapper component accepts each device as a prop, and then processes the ...

Is there a way to automatically validate v-forms inside a v-data-table when the page loads?

In my data entry form, I have utilized a v-data-table with each column containing a v-form and v-text-field for direct value updates. My goal is to validate all fields upon page load to identify any incorrect data inputs. However, I am facing challenges in ...

Using Angular 2 global pipes without requiring PLATFORM_PIPES

I was interested in utilizing a feature to create a global pipe and came across this link: https://angular.io/docs/ts/latest/api/core/index/PLATFORM_PIPES-let.html However, I discovered that it is deprecated with the following message: Providing platform ...

Interacting with the Dropdown feature on the page causes the body of the page to shift

I am encountering an issue with 2 dropdowns in Datatables used to filter content. The problem arises when a dropdown is positioned on the right side of the page, causing a shift to the left by approximately 10px. Conversely, if the dropdown is placed on th ...

Leveraging Next.js with TypeScript and babel-plugin-module-resolver for simplified import aliases

I am currently in the process of setting up a Next.js project with typescript. Despite following multiple guides, I have encountered an issue concerning import aliases. It's unclear whether this problem stems from my configuration or from Next.js its ...

Bring in jspm libraries to your project via typescript

While researching how to import jspm packages into typescript, I noticed that most examples assumed the use of SystemJS for loading and interpreting them in the browser. However, I prefer using tsc to compile commonjs modules and only import the js code, a ...

What is the best way to determine the type of `rootReducer`?

My project is set up with a combination of React, Redux, Immutable.js, and TypeScript. As I worked on implementing it, I made an effort to declare types wherever possible which led me to discover an interesting issue. A code example illustrating the proble ...

Retrieve the template parameter from a generic type

While I have experience extracting string from string[], this particular scenario is proving to be quite challenging: type example<T = boolean> = true; // with just "example", how can I retrieve the template parameter "boolean" in this case? type T ...

What is the method of duplicating an array using the array.push() function while ensuring no duplicate key values are

In the process of developing a food cart feature, I encountered an issue with my Array type cart and object products. Whenever I add a new product with a different value for a similar key, it ends up overwriting the existing values for all products in the ...