I find certain operations within certain types to be quite perplexing

I have defined two different types as follows:

interface ChangeAction{
    type: 'CHANGE'
    payload: string
}

interface DeleteAction {
    type: 'DELETE'
    payload: number
}

Now, I want to add a prefix to each value of the type key like ON. This is how I am doing it:

type MakePrefix<T extends {type: string}, P extends string> = Omit<T, 'type'> & {
    type: `${P}${T['type']}`
}

Usually, this method works perfectly fine. But when I use union types, it seems that some types are missing.

var changeAction: MakePrefix<ChangeAction, 'ON_'> = {
    type: 'ON_CHANGE', 
    payload: '12'
}

type OperateAction = MakePrefix<ChangeAction | DeleteAction, 'ON_'>

var operateAction: OperateAction = {
    type: 'ON_DELETE',
    payload: "123" // string | number ???
}

I am unsure if this issue is due to a bug or if my Type Operator is incorrect.

Answer №1

Check out this handy mapped type that can be used to add a prefix to the type prop (as well as other props):

TS Playground

type Prefix<Pre extends string, Str extends string> = `${Pre}${Str}`;

type PrefixStringProp<Pre extends string, Prop extends string, T extends Record<Prop, string>> = {
  [K in keyof T]: K extends Prop ? Prefix<Pre, T[K]> : T[K];
};

interface ChangeAction {
  type: 'CHANGE';
  payload: string;
}

interface DeleteAction {
  type: 'DELETE';
  payload: number;
}

const changeAction: PrefixStringProp<'ON_', 'type', ChangeAction> = {
  type: 'ON_CHANGE', 
  payload: '12',
}; // works fine

type OperateAction = PrefixStringProp<'ON_', 'type', ChangeAction | DeleteAction>

let operateAction: OperateAction;

operateAction = { /*
~~~~~~~~~~~~~
Type '{ type: "ON_DELETE"; payload: string; }' is not assignable to type 'OperateAction'.
  Types of property 'payload' are incompatible.
    Type 'string' is not assignable to type 'number'.(2322) */
  type: 'ON_DELETE',
  payload: '123',
};

operateAction = {
  type: 'ON_DELETE',
  payload: 123,
}; // also good

Answer №2

The issue you are encountering with

type MakePrefix<T extends { type: string }, P extends string> =
  Omit<T, 'type'> & { type: `${P}${T['type']}` }

lies in the fact that you anticipate it to distribute over unions within T, but it doesn't. You desire for MakePrefix<A | B | C, P> to be the same as

MakePrefix<A, P> | MakePrefix<B, P> | MakePrefix<C, P>
. While this seems like a valid expectation, certain type operations in TypeScript do not distribute this way. The Omit<T, K> utility type does not distribute across unions due to intentional design choices by Microsoft (as documented in microsoft/TypeScript#46361). Even if it were distributive, an intersection like F<T> & G<T> will not distribute over unions in
T</code, leading to unexpected outcomes. So, unfortunately, <code>MakePrefix<T, P>
does not behave in the intended manner. Your OperateAction is equivalent to:

type OperateAction = MakePrefix<ChangeAction | DeleteAction, 'ON_'>
/* type OperateAction = {
    payload: string | number;
    type: "ON_CHANGE" | "ON_DELETE";
   } */

And it permits the unwanted "cross-terms" such as:

var operateAction: OperateAction;
operateAction = { type: 'ON_DELETE', payload: 123 }; // okay
operateAction = { type: 'ON_CHANGE', payload: "abc" }; // okay
operateAction = { type: 'ON_DELETE', payload: "xyz" }; // okay?!

Fortunately, you can easily convert non-distributive type functions into distributive ones. A type function of the structure

type F<T> = T extends U ? G<T> : H<T>
represents a distributive conditional type, which automatically distributes over unions in
T</code. Therefore, if you have a non-distributive type <code>type NonDistrib<T> = X<T>
, you can create a distributive version by enclosing the definition in T extends any ? ... : never (or unknown or T in place of any), like so:
type Distrib<T> = T extends any ? X<T> : never
. Let's give it a shot:

type MakePrefix<T extends { type: string }, P extends string> =
  T extends unknown ? (
    Omit<T, 'type'> & { type: `${P}${T['type']}` }
  ) : never;

type OperateAction = MakePrefix<ChangeAction | DeleteAction, 'ON_'>;
/* type OperateAction = {
    payload: string;
    type: "ON_CHANGE";
} | {
    payload: number;
    type: "ON_DELETE";
} */

var operateAction: OperateAction;
operateAction = { type: 'ON_DELETE', payload: 123 }; // okay
operateAction = { type: 'ON_CHANGE', payload: "abc" }; // okay
operateAction = { type: 'ON_DELETE', payload: "xyz" }; // error, as desired

Looks promising. The revised OperateAction type aligns with

MakePrefix<ChangeAction, 'ON_'> | MakePrefix<DeleteAction, 'ON_'>
as intended.


That addresses the query at hand. In this specific scenario, I would consider simplifying into a naturally distributive form using the homomorphic mapped type instead of employing Omit and intersecting properties back. The homomorphic mapped type involves defining types using {[K in keyof XXX]: YYY} with in keyof, resulting in better clarity and ease of comprehension.

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

Oops! The program encountered an issue where it couldn't access the "Point" property because it was undefined. This occurred while working with openlayers 3 and jsts on

I am currently working on implementing a buffer function for some features that have been drawn on a map following this particular example. However, I am encountering the following error: ERROR TypeError: Cannot read property 'Point' of undefin ...

Printing values of an object in an Angular/HTML script is proving to be quite challenging for me

In my Angular project, I have created a service and component for listing objects. The list is functioning correctly as I have tested it with 'console.log()'. However, when I try to display the list on localhost:4200, nothing appears. <table&g ...

Cannot compile Angular 4 Material table: Encountering unexpected closing tag

Currently, I am working on an Angular 4 Project that involves using Material. The main task at hand is to implement a table from Angular Material. However, the issue I am facing is that the table does not compile as expected. Here's the HTML code sni ...

The name 'Firebase' is not recognized by Typescript

Encountering typescript errors while building a project that incorporates angularfire2 and firebase. Here are the packages: "angularfire2": "^2.0.0-beta.0", "firebase": "^2.4.2", Listed below are the errors: [10:58:34] Finished 'build.html_css&apos ...

What is the best way to retrieve class properties within an input change listener in Angular?

I am new to Angular and have a question regarding scopes. While I couldn't find an exact match for my question in previous queries, I will try to clarify it with the code snippet below: @Component({ selector: 'item-selector&apos ...

Issue with Click event not working on dynamically added button in Angular 8

My goal is to dynamically add and remove product images when a user clicks the add or delete button on the screen. However, I am encountering an issue where the function is not being called when dynamically injecting HTML and binding the click event. Below ...

Is there a method to enhance the efficiency of generating the code coverage report in VSTS?

What are the possible reasons for the extended duration (>1 min) required to generate the code coverage report when running the associated command in VSTS? Are there any strategies that can be implemented to streamline this process? ...

The module './$types' or its related type declarations could not be located in server.ts

Issue with locating RequestHandler in +server.ts file despite various troubleshooting attempts (recreating file, restarting servers, running svelte-check) +server.ts development code: import type { RequestHandler } from './$types' /** @type {imp ...

Using Google Fonts in a Typescript React application with JSS: A step-by-step guide

How can I add Google fonts to my JSS for use in styling? const styles = ({palette, typography}: Theme) => createStyles({ time: { flexBasis: '10%', flexShrink: 0, fontSize: typography.pxToRem(20) }, guestname: ...

Insert a new item into a current array using Typescript and Angular

-This is my curated list- export const FORMULARLIST: formular[] = [ { id: 1, name: 'Jane Doe', mobileNumber: 987654, secondMobileNumber: 456789, email: '<a href="/cdn-cgi/l/email-protection" class="__cf_email__" data-cfemail="e1bcc0d9ec ...

Having trouble sending a JSON object from Typescript to a Web API endpoint via POST request

When attempting to pass a JSON Object from a TypeScript POST call to a Web API method, I have encountered an issue. Fiddler indicates that the object has been successfully converted into JSON with the Content-Type set as 'application/JSON'. Howev ...

Is it possible to utilize an ng template within one component and then reference its template in another HTML file?

I'm experimenting with using ng-template in a separate component and referencing it in other parts of the html. Is this possible? I've tried different approaches but seem to be missing something. Can you help me understand where I might be going ...

Define the static property as an array containing instances of the same type

I created a class called Foo with a static property named instances that holds references to all instances. Then, I have another class called Bar which extends Foo: class Foo { static instances: Foo[]; fooProp = "foo"; constructor() { ...

Playwright script encounters module not found error

I am currently facing an issue with implementing Playwright in my project. It seems that Playwright is struggling to a) resolve path aliases and b) it is unable to locate certain npm packages that have been installed. Here is the structure of my project: ...

The interface 'Response<ResBody>' has been incorrectly extended by the interface 'Response'

I am currently working with typescript and express in a node.js environment. Whenever I compile my code, I encounter the following bug: node_modules/@types/express-serve-static-core/index.d.ts:505:18 - error TS2430: Interface 'Response<ResBody>& ...

Guide on how to import or merge JavaScript files depending on their references

As I work on my MVC 6 app, I am exploring a new approach to replacing the older js/css bundling & minification system. My goal is to generate a single javascript file that can be easily referenced in my HTML. However, this javascript file needs to be speci ...

Creating a personalized connect function in Typescript for react-redux applications

Currently, I am in the process of migrating a large and intricate application to Typescript. One specific challenge we are facing is our reliance on createProvider and the storeKey option for linking our containers to the store. With over 100 containers in ...

Utilizing a nested interface in Typescript allows for creating more complex and

My current interface is structured like this: export interface Foo { data?: Foo; bar?: boolean; } Depending on the scenario, data is used as foo.data.bar or foo.bar. However, when implementing the above interface, I encounter the error message: Prope ...

Conflicting events arising between the onMouseUp and onClick functions

I have a scrollbar on my page that I want to scroll by 40px when a button is clicked. Additionally, I want the scrolling to be continuous while holding down the same button. To achieve this functionality, I implemented an onClick event for a single 40px s ...

Accessing and sending only the body part of an HTTP response in Angular 7 test cases: A comprehensive guide

Currently, I am working on creating unit test cases in Angular 7 for a Component that utilizes an asynchronous service. This is the content of my component file: submitLoginForm() { if (this.loginForm.valid) { // send a http request to save t ...