Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

"TS2349: Cannot invoke an expression whose type lacks a call signature." when using union type containing a function #7960

Closed
josh-endries opened this issue Apr 8, 2016 · 13 comments
Labels
Canonical This issue contains a lengthy and complete description of a particular problem, solution, or design Question An issue which isn't directly actionable in code

Comments

@josh-endries
Copy link

TypeScript Version:

This is with TypeScript version 1.8.4.0 working in a TSX file in VS2015.

Code

The actual code is a big React component, so here is a sample with just the guts:

interface BarProps {
  failedContent?: string | Array<string> | JSX.Element | (() => JSX.Element);
}
class Bar extends React.Component<BarProps> {
  render() {
    let content = this.props.failedContent(); // TS2349
    //let content = (this.props.failedContent as () => JSX.Element)(); // Workaround
  }
}

Expected behavior:

I would expect it to "just work" in a "native" fashion, meaning the compiler recognizes that the property can be a function, so a function call isn't necessarily an incorrect usage, and that explicit casting isn't necessary. Is this syntax/functionality not supported, or am I writing it incorrectly maybe?

Actual behavior:

It generates a compiler error.

@Arnavion
Copy link
Contributor

Arnavion commented Apr 8, 2016

so a function call isn't necessarily an incorrect usage

But it is potentially an incorrect usage. The compiler only allows functions to be called, and you haven't convinced it that it's a function. If you run this code at runtime and the caller passes in a string for that prop, then this is going to blow up with a type error, which is exactly what TS is intended to prevent.

So the correct thing to do is to check whether the prop is a function or not and handle both cases accordingly.

interface BarProps {
  failedContent?: string | string[] | (() => string);
}
class Bar {
    props: BarProps;

    render() {
        const { failedContent } = this.props;
        if (typeof failedContent === "function" && !(failedContent instanceof Array) {
            let content = failedContent();
        }
        else {
            throw new Error("Expected failedContent prop to be a function.");
        }
    }
}

Note:

  • Type guards only function with simple bindings (variables, function parameters), not expressions, so you can't use this.props.failedContent directly.
  • The typeof === "function" type guard should be sufficient on its own, but for some reason TS doesn't remove the array type with just that.
  • In the current 1.8 release the throw does not prevent the rest of the render function from also getting the narrowed type. This is being fixed (among other things) in the new type guards implementation.

@mhegazy mhegazy closed this as completed Apr 8, 2016
@mhegazy mhegazy added Question An issue which isn't directly actionable in code Canonical This issue contains a lengthy and complete description of a particular problem, solution, or design labels Apr 8, 2016
@josh-endries
Copy link
Author

Thanks, I didn't know about your first note, that helped me fix a couple other compiler errors. What you wrote is almost exactly what I have, but my call is within a switch (typeof failedContent), which evidently the compiler doesn't pick up for stripping off the other types. Neither does caching the type in a pcType variable, I guess you must use typeof x directly. Bleh.

In a similar problem (the next prop down) it also can't distinguish between JSX.Element | () => JSX.Element. In JS, the former is an object and the latter is a function. With the former I'm trying to indicate an instance of ReactElement, and the latter a function that returns a ReactElement. However in both typeof x === 'object' and typeof x === 'function' the compiler includes both types in the list. Is there a way to distinguish between them in TS?

@Arnavion
Copy link
Contributor

Arnavion commented Apr 9, 2016

switch for type guards is #2214


Your second point is the same as my second note, i.e. that typeof === "function" doesn't strip the object type. For Array I could get it to work by doing instanceof Array but since JSX.Element is an interface that doesn't work for it.

One way is to check typeof === "object" and assert that as a test for whether it's a JSX.Element or not using user-defined type guards.

        if (((arg): arg is JSX.Element => typeof arg === "object")(failedContent)) {
            let content = failedContent; // JSX.Element
        }

The other simpler way is just assert it as a JSX.Element:

        if (typeof failedContent === "object") {
            let content = failedContent as JSX.Element;
        }

Perhaps open a separate issue for why typeof === "function" doesn't strip object types and vice versa.

@cawa-93
Copy link

cawa-93 commented Sep 7, 2016

The following code also results in an error TS2349:

if (typeof this.method === 'function') {
    let method = this.method(); // TS2349
} else {
    let method = this.method; // Work
}

let method = typeof this.method === 'function' ? this.method() : this.method; // TS2349

The method has been declared in the class:

export class DeamonService {
    private method: string | (() => string);
    // ... other code
}

This only applies to a class method. If you create the same variable, the compiler calculates the correct type

var method = angular.copy(this.method);
// method : string | (() => string)
if (typeof method === 'function') {
    method();      // Work
}
if (typeof this.method === 'function') {
    this.method(); // TS2349
}

@mhegazy
Copy link
Contributor

mhegazy commented Sep 7, 2016

@cawa-93 this should be working as intended in TS 2.0 and later. please give it a try.

@cawa-93
Copy link

cawa-93 commented Sep 7, 2016

@mhegazy, Yes. With tsc@2.0 there are no error.

@pfurini
Copy link

pfurini commented Oct 19, 2016

Using TS 2.0, this doesn't work either:

getFromRepo(ids: string | number | string[] | number[]): Promise<any | any[]> {
        if (Array.isArray(ids)) {
            const promises: Promise<any>[] = [];
            (ids as string[] | number[]).forEach((id: string | number) => {  // TS2349
                promises.push(this.repository.find(id));
            });
            return Promise.resolve(Promise.all(promises));
        }
        else {
            return this.repository.find(ids as string | number);
        }
}

I can't find any way to have that forEach call working, even ids instanceof Array isn't recognized as a type guard. This is very annoying..
Array.isArray() call is fairly standard way to check for an array, and TS should support it as a type guard.

@RyanCavanaugh
Copy link
Member

@Nexbit see #7294

@pfurini
Copy link

pfurini commented Oct 19, 2016

@RyanCavanaugh Ok, refactored as (ids as any[]).forEach(id: string | number) => {}) works fine and gives type safety where it's needed..
But as a side question, is the Array.isArray() call supported as type guard by the compiler? Thx..

@RyanCavanaugh
Copy link
Member

Yes
image

@Arnavion
Copy link
Contributor

You can get better type safety with (ids as (string | number)[]).forEach(id => {}). No need to drop down to any

@pfurini
Copy link

pfurini commented Oct 19, 2016

@Arnavion you're right indeed, thanks! tried everything, and missed this..

@samuelmtimbo
Copy link

Sorry to bring this back.

import { Readable, Transform, Writable } from 'stream'

function unpipe(output: Readable | Transform, input: Writable): void {
  output.unpipe<Writable>(input)
}

error TS2349: Cannot invoke an expression whose type lacks a call signature. Type '((destination?: T) => Readable) | ((destinati...' has no compatible call signatures.

I can't understand why I am getting this error, since both Readable and Transform have the unpipe method.

sks added a commit to sks/vuex that referenced this issue Mar 26, 2018
- while testing we have methods using this method signature and it would be nice if we did not have define this.

We were running into the issue as reported in microsoft/TypeScript#7960

So workaround was to do something like
```
const loginAction = userActions.login as ModuleAction<UserState>;
```
ktsn pushed a commit to vuejs/vuex that referenced this issue Apr 2, 2018
- while testing we have methods using this method signature and it would be nice if we did not have define this.

We were running into the issue as reported in microsoft/TypeScript#7960

So workaround was to do something like
```
const loginAction = userActions.login as ModuleAction<UserState>;
```
@microsoft microsoft locked and limited conversation to collaborators Jul 3, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Canonical This issue contains a lengthy and complete description of a particular problem, solution, or design Question An issue which isn't directly actionable in code
Projects
None yet
Development

No branches or pull requests

7 participants