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

Type discrimination broken on generic, mapped type values when strictNullChecks:false #41530

Open
ChuckJonas opened this issue Nov 13, 2020 · 3 comments
Labels
Bug A bug in TypeScript
Milestone

Comments

@ChuckJonas
Copy link

ChuckJonas commented Nov 13, 2020

Running into what I think is a bug with type discrimination of a function when you have a mapped type on a generic type . I've tried to make the reproduction as simple as possible.

TypeScript Version: 4.1.0-beta

Search Terms:
discrimination, generic, mapped type, strictNullChecks off

Code

type Mapping<T> = { [key in keyof T]?: string | (() => string) };

function foo<T>(mapping: Mapping<T>) {
    const results: {[key: string]: string} = {};
    for (const key in mapping) {
      const valueTransform = mapping[key];
      if (typeof valueTransform === 'function') {
        results[key] = valueTransform(); //! This expression is not callable...
      } else if(typeof valueTransform === 'string') {
        results[key] = valueTransform;
      }
    }
    return results;
}

Expected behavior:

Actual behavior:
strictNullChecks:false I get the following error:

This expression is not callable.
  Not all constituents of type 'string | (() => string)' are callable.
    Type 'string' has no call signatures.

If you change it back to true, it will work as expected. It seems to have something to do with the combination of generics & mapped types, as if you change to reference Foo directly the code will compile.

type Mapping<T> = { [key in keyof Foo]?: string | (() => string) };

Also the discrimination only seems to break on the typeof valueTransform === 'function'

Playground Link: Playground Link

Related Issues:
#27470

@RyanCavanaugh RyanCavanaugh added the Bug A bug in TypeScript label Nov 13, 2020
@RyanCavanaugh RyanCavanaugh added this to the Backlog milestone Nov 13, 2020
@web-padawan
Copy link

The issue can be reproduced with the following example:

function runIfFunction<T>(value: T | (() => T)): T {
  if (typeof value === 'function') {
    return value();
  } else {
    return value;
  }
}

The error looks as follows:

This expression is not callable. Not all constituents of type '(() => T) | (T & Function)' are callable. 
Type 'T & Function' has no call signatures.

When changing the type guard from typeof value === 'function' to value instanceof Function it works.

@dragomirtitian
Copy link
Contributor

dragomirtitian commented Jan 21, 2021

@web-padawan I would argue in the generic case the insatnceof behavior is wrong and this should be an error. If T is (a: string) => string so the type guard does not exclude T from the true branch:

function runIfFunction<T>(value: T | (() => T)): T {
  if (typeof value === 'function') {
    return value();
  } else {
    return value;
  }
}


runIfFunction<(a: string) => string>((a: string) => a.toLocaleLowerCase())

Playground Link

@turtleflyer
Copy link

Another sample:

type A0<A0T> = Exclude<A0T, Function>;
type A1<A1T> = Exclude<A1T, Function> | Function;

function f<XF>(x: XF): void {
  type AF0 = A0<XF>
  type AF1 = A1<XF>

  const y0 = {} as AF0;
  const y1 = {} as AF1;

  const z0 = typeof y0 === 'function' ? y0() : undefined;
  const z1 = typeof y1 === 'function' ? y1() : undefined;
  /*                                   ^^^^^
  This expression is not callable.
  No constituent of type 'Function | (Exclude<XF, Function> & Function)' is callable.(2349)
  */

  const r = typeof y1 === 'function' ? y1 : null as never;
  type R = typeof r; // type R = Function
}

Play

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript
Projects
None yet
Development

No branches or pull requests

5 participants