What causes loss of inference following a pick operation?

Consider this scenario:

type THandlerArgsBase<TValue> = {
    value: TValue | undefined;
}

type THandlerArgsUnnamed<TValue> = THandlerArgsBase<TValue> & {
    name: undefined;
}

type THandlerArgsNamed<TValue> = THandlerArgsBase<TValue> & {
    name: string;
}



type TProvaBase<TValue> = {
    value?: TValue | undefined;
    foo?: string;
    blah?: number;
}

type TProvaUnnamed<TValue> = TProvaBase<TValue> & {
    name?: undefined;
    handler?: (args: THandlerArgsUnnamed<TValue>) => void;
}

type TProvaNamed<TValue> = TProvaBase<TValue> & {
    name: string;
    handler?: (args: THandlerArgsNamed<TValue>) => void;
}

type TProva<TValue> =
    | TProvaUnnamed<TValue>
    | TProvaNamed<TValue>
    ;


//generic implementation
function prova<TValue>(props: TProva<TValue>): void {
    const {
        value,
        name,
        handler,
    } = props;

    if (handler) {
        typeof name === "string"
            ? handler({ name, value })
            : handler({ name, value })
    }
}


//usage
prova<number>({
    name: "xyz",
    handler: ({ name, value }) => void 0,  //name is string: correct!
})

prova<number>({
    handler: ({ name, value }) => void 0,  //name is undefined: correct!
})


//specialization

type TProvaBool = Pick<TProva<boolean>, "value" | "name" | "handler">;

function prova_bool(props: TProvaBool): void {
    const {
        value,
        name,
        handler,
    } = props;

    if (handler) {
        typeof name === "string"
            ? handler({ name, value })    //compilation error!
            : handler({ name, value })    //compilation error!
    }
}

In the latter method, all but one field are correctly inferred. The handler field is unexpectedly inferred as:

handler: (args: never) => void

Interestingly, when I remove the Pick operation to specialize the generic function, everything works fine:

type TProvaBool = TProva<boolean>;

I tested this in the Typescript playground (v5.0.4).

Why does inference break after using Pick to specialize the generic function?

Is there a workaround for this issue?

Answer №1

The issue arises from the fact that the `Pick` utility type does not distribute evenly across unions in `T`. This means that `Pick` is not equivalent to combining `Pick`, `Pick`, and `Pick`. According to microsoft/TypeScript#28339, TypeScript intentionally behaves this way because some users may not want similar structures to propagate across unions.

Therefore, when using `Pick` with a union type `T`, you end up with a unified object type where each property becomes a union.

type A = { x: 1, y: 2 } | { x: true, y: false } | { x: "yes", y: "no" };
type PickAx = Pick<A, "x">;
// type PickAx = { x: true | 1 | "yes"; }

If you desire distributive behavior for `Pick`, you can create your own version as shown in the current definition provided here.

type DistribPick<T, K extends keyof T> =
  T extends unknown ? { [P in K]: T[P] } : never

By wrapping it in a distributive conditional type, we achieve distributive behavior:

type DistribPickAx = DistribPick<A, "x">
// type DistribPickAx = { x: 1; } | { x: true; } | { x: "yes"; }

This output aligns more closely with what was initially expected.


Applying this concept to your example yields:

type TProvaBool = DistribPick<TProva<boolean>, "value" | "name" | "handler">;
/* type TProvaBool = {
    value?: boolean | undefined;
    name?: undefined;
    handler?: ((args: THandlerArgsUnnamed<boolean>) => void) | undefined;
} | {
    value?: boolean | undefined;
    name: string;
    handler?: ((args: THandlerArgsNamed<boolean>) => void) | undefined;
} */

As a result, any compilation errors are resolved:

function prova_bool(props: TProvaBool): void {
  const {
    value,
    name,
    handler,
  } = props;

  if (handler) {
    typeof name === "string"
      ? handler({ name, value }) // ok
      : handler({ name, value }) // ok
  }
}

Feel free to check out the code on the Playground link

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

Methods in Ionic to call an external JavaScript file from TypeScript

Having a JSON list stored in a JavaScript file, my objective is to filter it using TypeScript before sending the filtered results to the HTML homepage. However, I encountered an issue within the HTML file. It's worth mentioning that when running the ...

Creating a dynamic path to an imported file in React: A step-by-step guide

Struggling with a dilemma regarding dynamically generated paths for importing files in React. I have utilized the map() function to generate a dynamic part of the code, consisting of a repetitive sequence of div elements, each housing an audio element. The ...

A step-by-step guide on extracting information from a component and using it within a Guard

A challenge I am facing is how to display a warning prompt on the screen if the user tries to leave a page after making changes to a form. I have successfully created a Guard that logs a message to the console when the user attempts to navigate away from t ...

"Addressing the challenge of deploying Typescript GraphQL in a live production

For the past week, I've been struggling to research and deploy my server into production. My setup includes Typescript, Nexus GraphQL, Prisma with docker-compose. During development, everything runs smoothly using ts-node-dev. However, when attempting ...

Issues with compiling arise post downloading the latest Angular 2 quickstart files

My Angular 2 project was running smoothly on version 2.3, but I decided to upgrade to version 2.4. To do so, I downloaded the latest quickstart files from https://github.com/angular/quickstart After replacing my tsconfig.json, package.json, and systemjs.c ...

Encountering an issue with the nativescript-carousel due to tns-platform-declarations

While constructing a nativescript carousel using the nativescript-carousel plug-in, I encountered an issue when running tns build android. The error message reads as follows: node_modules/nativescript-carousel/index.d.ts(1,22): error TS6053: File 'D: ...

Manage InversifyJS setup based on the current environment

Recently, I've been utilizing InversifyJS to manage dependency injection within my TypeScript server. A current challenge I face is the need to inject varying implementations into my code based on the environment in which it is running. A common situ ...

How to add icons to HTML select options using Angular

Looking for a way to enhance my component that displays a list of accounts with not only the account number, but also the currency represented by an icon or image of a country flag. Here is my current component setup: <select name="drpAccounts" class= ...

Create duplicates of both the array and its individual elements by value

I'm having trouble figuring out how to properly copy an array along with all its elements. In my model, I have a name and options, both of which are strings. This is what I currently have: const myArrayToDuplicate = [myModel, myModel, myModel] My ...

Encountering a problem when attempting to iterate through Observable Objects in Angular 2

I've hit a roadblock trying to iterate through the observable object in my users service. The error thrown by Chrome's console is: error_handler.js:47 EXCEPTION: undefined is not a function Below is the code causing the issue: users.compone ...

Methods for comparing the current date and time to a specific time stored in a database

A database table contains the following values: "295fc51f6b02d01d54a808938df736ed" : { "author" : "James Iva", "authorID" : "JBvLC3tCYCgFeIpKjGtSwBJ2scu1", "geometry" : { "latitude" : 29.4241219, "longitude" : -98.49362819999999 ...

Unable to instantiate an Angular component constructor using a string parameter

So, I've created a simple component: export class PlaintextComponent implements OnInit { schema: PlaintextTagSchema; constructor(private _ngZone: NgZone, prompt: string, maxRows: number, maxChars: number) { this.schema.prompt = prompt; t ...

Struggling with the 'formControl' binding issue in Angular 2 Material Autocomplete? Learn how to resolve the problem with this step-by-step guide

I am attempting to incorporate the Angular Material Autocomplete component into my Angular 2 project. Here is how I have added it to my template: <md-input-container> <input mdInput placeholder="Category" [mdAutocomplete]="auto" [formControl]= ...

Verify if a string containing only numerical values contains any non-numeric characters

I have an input field for a phone number, but the type is set to text instead of number. I want to validate it if there are any non-numeric characters present, since I have formatted the input to look like this: 123-123-1234 Here is my input: <input (ke ...

Module import in Ionic

I'm encountering an issue with Ionic, Angular, and TypeScript, and I'm feeling a bit lost... I'm trying to call an external function from my file but I keep getting the following error: "Uncaught (in promise): TypeError: undefined is not an ...

Can you explain the purpose of the lodash get function?

I'm struggling to understand the purpose of this specific line of code: const loanPeriod: number = get(product, 'TermMonths', this.defaultTerm) / this.monthsInAYear; The variables defaultTerm and monthsInAYear are defined globally. The prod ...

Angular - Set value only if property is present

Check if the 'rowData' property exists and assign a value. Can we approach it like this? if(this.tableObj.hasOwnProperty('rowData')) { this.tableObj.rowData = this.defVal.rowData; } I encountered an error when attempting this, specif ...

Retrieving a variable value set within a jQuery function from within an Angular 2 component

In the current project, I am facing a situation where I need to work around and initialize jQuery datetimepicker inside an Angular 2 application (with plans to refactor it later). However, when I assign a datetime value to a variable, I encounter a proble ...

What is the process for setting up a node test launch configuration?

I have some "node 18 test runner" tests ready to be executed. I can run them using the following command: node --loader tsx --test tests/**/*.ts To debug these tests in vscode, I realized that I need to set up a configuration entry in my launch.json. But ...

Unable to see Next.js page in the browser window

I have set up a next.js project with App Router and my file structure under the app folder looks like this: *some other files* . . user | [id] | | page.tsx | @users | | page.tsx | layout.tsx | page.tsx I am ...