What is the reason behind TypeScript expecting 'never' as a function argument when retrieving the function type using generics?

I need some assistance with transitioning from TS 2.6 to 3.4 as I am encountering unexpected issues. Previously, my code was functioning correctly, but now I am facing a compiler error:

type MyNumberType = 'never' | 'gonna';
type MyStringType = 'give' | 'you';
type MyBooleanType = 'up' 

interface Bar {
    foo(key: MyNumberType): number;
    bar(key: MyStringType): string;
    baz(key: MyBooleanType): boolean;
}

function test<T extends keyof Bar>(bar: Bar, fn: T) {
    let arg: Parameters<Bar[T]>[0];
    bar[fn](arg); // error here
}

The exact error message is:

Argument of type 'Parameters<Bar[T]>' is not assignable to parameter of type 'never'.
Type 'unknown[]' is not assignable to type 'never'.
Type '[number] | [string] | [boolean]' is not assignable to type 'never'.
Type '[number]' is not assignable to type 'never'.

When checking this on the TypeScript playground, I see that the function expects the argument to be of type 'never': https://i.sstatic.net/uQPfJ.png

This issue is perplexing to me because there is only one function argument, and its type should be inferred using Parameters. Why then, does the function anticipate never?

Answer №1

The issue lies in the fact that the compiler is unable to recognize that arg and bar[fn] are interconnected. Instead, it treats both of them as independent union types, leading to an expectation that all combinations of union constituents are valid, even though most combinations are not.

In TypeScript 3.2, you would have received an error message stating that bar[fn] does not possess a call signature due to being a union of functions with distinct parameters. It's unlikely that this code functioned correctly in TS2.6; especially since the use of Parameters<> was not present until conditional types were introduced in TS2.8. I attempted to refactor your code to be compatible with TS2.6 using:

interface B {
    foo: MyNumberType,
    bar: MyStringType,
    baz:MyBooleanType
}

function test<T extends keyof Bar>(bar: Bar, fn: T) {
    let arg: B[T]=null!
    bar[fn](arg); // this line triggers an error
}

I also tested this code in TS2.7 which still resulted in an error. Thus, it seems that the original code never functioned properly.

Regarding the never complication: TypeScript 3.3 brought about enhanced support for calling unions of functions. This required the parameters to represent the intersection of parameters from the union of functions. While this is beneficial in some scenarios, in your case, the parameter must signify the intersection of several unique string literals, resulting in collapsing down to never, essentially reflecting the same "cannot call this" error but in a more complex manner.


To address this effectively, one solution is to utilize a type assertion, as you possess more insight than the compiler in this particular instance:

function test<T extends keyof Bar>(bar: Bar, fn: T) {
    let arg: Parameters<Bar[T]>[0] = null!; // assign a value
    // Assert that bar[fn] accepts a union of arguments and returns a union of results
    (bar[fn] as (x: typeof arg) => ReturnType<Bar[T]>)(arg); // no error
}

A type assertion does introduce risk, allowing you to mislead the compiler:

function evilTest<T extends keyof Bar>(bar: Bar, fn: T) {
    // The assertion below deceives the compiler
    (bar[fn] as (x: Parameters<Bar[T]>[0]) => ReturnType<Bar[T]>)("up"); // no error!
}

Therefore, caution should be exercised. While there exists a method to create a completely type-safe version, involving coercing the compiler into executing code flow analysis on every potentiality:

function manualTest<T extends keyof Bar>(bar: Bar, fn: T): ReturnType<Bar[T]>;
// Unions can be narrowed, generics cannot
// Refer to https://github.com/Microsoft/TypeScript/issues/13995
// and https://github.com/microsoft/TypeScript/issues/24085
function manualTest(bar: Bar, fn: keyof Bar) {
    switch (fn) {
        case 'foo': {
            let arg: Parameters<Bar[typeof fn]>[0] = null!
            return bar[fn](arg);
        }
        case 'bar': {
            let arg: Parameters<Bar[typeof fn]>[0] = null!
            return bar[fn](arg);
        }
        case 'baz': {
            let arg: Parameters<Bar[typeof fn]>[0] = null!
            return bar[fn](arg);
        }
        default:
            return assertUnreachable(fn);
    }
}

Although this approach remains fragile (necessitating modifications if additional methods are appended to Bar) and repetitive (repeating identical clauses), I typically lean towards the type assertion technique discussed earlier.

Hopefully, this information proves helpful; best of luck!

Answer №2

The manner in which barFn is defined is essentially similar to:

type Foo = (key: number) => number;
type Bar = (key: string) => string;
type Baz = (key: boolean) => boolean;

function test1(barFn: Foo | Bar | Baz) {    
    barFn("a"); // error will be encountered here
}

The parameter of barFn will actually involve an intersection of types instead of a union. The type never serves as a bottom type due to the absence of any intersection among number, string, and boolean.

It is recommended to define barFn as an intersection of the three function types, for example:

function test2(barFn: Foo & Bar & Baz) {    
    barFn("a"); // no error will occur here
}

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

Is there a way to ensure that the onChange event of ionic-selectable is triggered twice?

I've been working with an ionic app that utilizes the ionic-selectable plugin, and for the most part, it's been running smoothly. However, I encountered a rare scenario where if a user on a slow device quickly clicks on a selection twice in succe ...

Leveraging Global Variables in TypeScript with Node Express

Having issues using global variables in node express when working with typescript. Specifically, trying to use the same global array variable tokenList:tokList across multiple modules and middleware, but haven't been successful so far. Any suggestions ...

When using TypeORM's findOneBy method, if the search result

In order for the entity to have both identifiers, I require it to possess the Id and the _id export class ScriptSequencesExecutionEntity { @PrimaryGeneratedColumn({ name: 'id' }) _id!: string; @ObjectIdColumn() id: number; @AutoMap() ...

Develop a TypeScript Module that consolidates all exports

My Goal with TypeScript Modules I aim to streamline my TypeScript project by creating a module that contains all the necessary class exports. Currently, I find myself using relative import statements in my classes, which can make maintenance challenging i ...

Validation of emails in Angular through the utilization of reactive forms

Hello there, I'm new to Angular and looking for some assistance. Specifically, I am currently working on validating an email input using a reactive form with the keyup event. registerform:any; ngOnInit(): void { this.registerform = new F ...

Error message: "Supabase connection is returning an undefined value

I am encountering an issue with my Vercel deployed Remix project that utilizes Supabase on the backend, Postgresql, and Prisma as the ORM. Despite setting up connection pooling and a direct connection to Supabase, I keep receiving the following error whene ...

The Angular route successfully navigated to the page, but the HTML content was not

Whenever I select the Home option in the navigation bar, it takes me to the home URL but doesn't display the HTML content. Below is my app.routing.module.ts code: import { Component, NgModule } from '@angular/core'; import { RouterModule, Ro ...

When attempting to open an Angular modal window that contains a Radio Button group, an error may occur with the message "ExpressionChanged

I am brand new to Angular and have been trying to grasp the concept of lifecycle hooks, but it seems like I'm missing something. In my current project, there is a Radio Button Group nested inside a modal window. This modal is triggered by a button cl ...

The TypeScript library React-Time-Ago seems to require a number, but I'm passing it a string instead. I'm struggling to find a way to make it accept a number

import ReactTimeAgo from "react-time-ago" <ReactTimeAgo date = {tweet._createdAt} /> _createdAt displays time in the format 2022-06-02T01:16:40Z To convert this into a more human-readable form like " ...

Tips for retrieving the image source value with TypeScript

I've been attempting to access a JSON API in order to fetch some specific data such as titles, categories, and images. The issue I'm facing revolves around retrieving the image sources. Here's what I've tried: field_image[0].src fie ...

Is there a way to incorporate new members into a pre-existing type in TypeScript?

While examining the definition file for the commander project, one can observe the following interface being utilized: interface IExportedCommand extends ICommand { Command: commander.ICommandStatic; Option: commander.IOptionStatic; [key: stri ...

Encountering an issue in Angular 4 AOT Compilation when trying to load a Dynamic Component: Error message states that the

My Angular application is facing challenges when loading dynamic components. Everything works smoothly with the JIT ng serve or ng build compilation. Even with AOT ng serve --prod or ng build --prod, I can still successfully build the application. Lazy loa ...

Develop a custom data structure by combining different key elements with diverse property values

Within my coding dilemma lies a Union of strings: type Keys = 'a' | 'b' | 'c' My desire is to craft an object type using these union keys, with the flexibility for assigned values in that type. The typical approach involves ...

Ways to locate an element using the OR operator in the locator

Testing 2 very similar elements on different pages: <input name="myName"> <input name="name"> My attempt to locate the input element using Or (||) keyword was unsuccessful. Is it possible to achieve this without xpath, sol ...

The attribute "at" is not a valid property for an Array

As per the documentation on MDN Web Docs Array.prototype#at method, it should be a valid function. However, when attempting to compile in TypeScript, an error occurs stating that the method does not exist. public at(index: number): V { index = Math.floor ...

Determine whether or not there are any duplicate elements within an array object

Is there a way to determine true or false if there are any duplicates within an object array? arr = [ { nr:10, name: 'aba' }, { nr:11, name: 'cba' }, { nr:10, name: 'aba' } ] arr2 = [ { year:2020, cit ...

Is it feasible to obtain the parameter types of a function in Typescript?

Illustration: function a(...args: ???type of b() params???) { b(...args) } The goal is for args to match the type of parameters in function b. This idea is aimed at improving code readability and encapsulation. The function b is imported, so I don&apo ...

The script type `(close: any) => Element` cannot be assigned to type `ReactNode`

Encountering a problem while adding a popup in my TypeScript code Error: Type '(close: any) => Element' is not compatible with type 'ReactNode'. <Popup trigger={ <button className="fixed bott ...

The type 'Observable<Response>' does not include a property called 'map'

I recently started learning angular and npm, but encountered an error while trying to replicate some code from a source I found (linked here). It seems like the error is related to my development environment, but I'm having trouble pinpointing the ex ...

Using 'expect', 'toBe', and 'toEqual' in Angular 2 - A step-by-step guide!

Looking to implement something I came across that resembles this: let item1 = {value:5}; let item2 = {value:5}; // Should result in true. expect(item1).toEqual(item2); Unfortunately, an error is being thrown: EXCEPTION: Error in :0:0 caused by: expect ...