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

Created on 8 Apr 2016  路  13Comments  路  Source: microsoft/TypeScript

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.

Canonical Question

Most helpful comment

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

All 13 comments

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.

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?

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.

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
}

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

@mhegazy, Yes. With [email protected] there are no error.

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.

@nexbit see #7294

@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..

Yes
image

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

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

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.

Was this page helpful?
0 / 5 - 0 ratings