Skip to content

Regression: TypeScript no longer infers primitive types as widened types in conditional types. #29825

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

Closed
ghost opened this issue Feb 8, 2019 · 3 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@ghost
Copy link

ghost commented Feb 8, 2019

TypeScript Version: 3.1.0-dev.20180918 <= affected version <= 3.4.0-dev.20190208

Search Terms:
Issues, PRs + Google: infer[ence] literal type
Searched the changelog of TS 3.1 to find out whether this might be an intended change.

Code

class Test<T> {
  makePromise<V>(value: T extends Promise<V> ? V : never): Promise<V> {
    return Promise.resolve(value);
  }
}

const test = new Test<Promise<number>>();
test.makePromise(5) // throws a compile-time error starting with [email protected]

Obs: infer V rather than <V> works across all versions in the above example.

Expected behavior:
In the above example V should be inferred to a primitive type (e.g. number in the given example but also applies to other primitive types)

Actual behavior:
Starting with [email protected], TypeScript infers the value in the above type to a literal type. The last working package is [email protected] (including 2.x and 3.x)

Playground Link:
https://www.typescriptlang.org/play/#src=class%20Test%3CT%3E%20%7B%0D%0A%20%20makePromise%3CV%3E(value%3A%20T%20extends%20Promise%3CV%3E%20%3F%20V%20%3A%20never)%3A%20Promise%3CV%3E%20%7B%0D%0A%20%20%20%20return%20Promise.resolve(value)%3B%0D%0A%20%20%7D%0D%0A%7D%0D%0A%0D%0Aconst%20test%20%3D%20new%20Test%3CPromise%3Cnumber%3E%3E()%3B%0D%0Atest.makePromise(5)%0D%0A

Related Issues:
none found

@RyanCavanaugh
Copy link
Member

This is intended change.

The core problem here is that Promise's measured variance is backwards from what it "should" be - it appears to accept Ts but actually produces them. The user-side workaround is just to flip around the conditional type:

type Consumer<T> = {
  take(arg: T): void;
}

type Producer<T> = {
  produce(): T;
}

class Foo<T> {
  makeConsumer<U>(val: T extends Consumer<U> ? U : never): Consumer<U> {
    return {
      take(arg) {

      }
    }
  }

  // Fixed
  makeProducer<U>(val: Producer<U> extends T ? U : "other branch"): Producer<U> {
    return {
      produce() {
        return val as any;
      }
    }
  }
}

const f = new Foo<Consumer<number>>();
f.makeConsumer(10);

const g = new Foo<Producer<number>>();
g.makeProducer(10);

@ghost
Copy link
Author

ghost commented Feb 19, 2019

Sorry, unfortunately I didn't get it. You seem to suggest the following:

class Test<T> {
  // Switching around the conditional type as you propose:
  makePromise<V>(value: Promise<V> extends T ? V : never): Promise<V> {
    return Promise.resolve(value);
  }
}

This version will always enter the "other branch" if T subclasses Promise (as is intended obviously). Do you agree that this doesn't make sense?

@RyanCavanaugh: Could you please apply your solution to the original example so that it becomes clear how it should be rewritten? I think it would be great for others who stumble upon this ticket as well.

@typescript-bot
Copy link
Collaborator

This issue has been marked 'Working as Intended' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

2 participants