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

Wrong type inference from optional generic types #49874

Closed
incepter opened this issue Jul 12, 2022 · 8 comments
Closed

Wrong type inference from optional generic types #49874

incepter opened this issue Jul 12, 2022 · 8 comments

Comments

@incepter
Copy link

incepter commented Jul 12, 2022

Bug Report

Type inference from optional generics type become wrong in all major IDEs when descructuring.

I accidently discovered this behavior in a project im working on that's using a third party library: I used to have an excellent type inference when using expressions like this:

const {value: {nested}} = constructObject(src);

Let's say constructObject<T, E = Value<T>> is a function that operates on type T while returning a portion of it as E, that when omitted, means that we are using Value.

I did not have any issues before 4.7 and all my IDE's (intellij, vscode and codesandbox) were reporting it fine.

Now, and only when desctructuring, rather than using the optional Value<T> as type, it tries to infer it from the descructuring I am making and try to invest a new type for that.

🔎 Search Terms

Wrong type inference
type inference on optional generic types
...

🕗 Version & Regression Information

I did not pay attention to the first time this occurred, I used to have exact and correct type inference in 4.6.x and I did notice this behavior unless 4.7+

⏯ Playground Link

This codesandbox.

💻 Code

type Value<T> = { t: T }
type Test<T, E = Value<T>> = { value: E }

function constructValue<T>(value: T): Value<T> {
  return {t: value}
}

function constructTest<T, E = Value<T>>(
  value: T, sl?: (e: T) => E): Test<T, E> {

  return {
    // ts warning: 'E' could be instantiated with an arbitrary type which could be unrelated to 'Value<T> | E'.
    // IDE says value is of type 'E = value<T>'
    value: typeof sl === "function" ? sl(value) : constructValue(value)
  }
}

// if we assign then destructure later, it s fine
const result = constructTest(5)
const {value: {t: t1}} = result; // t: number
// --> destructuring directly make it try to create the optional parameter
// rather than using the default one
const {value: {t: t2}} = constructTest(5); // t: any

// if we assign then destructure later, it s fine
const {value} = constructTest({hello: "world"}); // value: Value<{hello: string}>
const {t: {hello}} = value; // t: {hello: string}
// --> destructuring directly make it try to create the optional parameter
// rather than using the default one
const {value: {t: t3}} = constructTest({hello: "world"}); // t: any

// adding the selector that syncs the optional generic type seems to work as expected
const {value: {override: o1}} = constructTest(5, e => ({override: e})); // override: number
const {value: {override: o2}} = constructTest(5, e => ({override: e.toString()})); // override: string

🙁 Actual behavior

It tries to invent a type from what i'm destructuring rather than using the optional generic type.

🙂 Expected behavior

If omitting the optional type, this means: use the default one, not invent one.

@incepter
Copy link
Author

I also created few days ago this stackoverflow question, but without luck.

@MartinJohns
Copy link
Contributor

Here's a playground link, because the provided sandbox is using TypeScript 4.1.3.

@MartinJohns
Copy link
Contributor

The error is correct. When sl is undefined, you use constructValue(value) and to create a value typed T, and try to return this with the return type being E. But T and E can be completely unrelated types.

Here's an example:

const res = constructTest<string, number>('abc')
console.log(res.value)

The type of res.value is number, but the value is actually the string "abc".

@MartinJohns
Copy link
Contributor

One solution is to use overloads to prevent the situation from sl being undefined and having an incompatible type for E, and also return two distinct object literals to prevent the type of the object literal to be incompatible.

function constructTest<T>(value: T): Test<T, Value<T>>;
function constructTest<T, E = Value<T>>(value: T, sl: (e: T) => E): Test<T, E>;
function constructTest<T, E = Value<T>>(value: T, sl?: (e: T) => E): Test<T, Value<T>> | Test<T, E> {
  return typeof sl === 'function'
    ? { value: sl(value) }
    : { value: constructValue(value) };
}

@incepter
Copy link
Author

incepter commented Jul 12, 2022

Thanks @MartinJohns for your answers.

I guess I understand the danger with T and E being unrelated, and this should be (in our case) a developer concern and the IDE will still help in it.

But, I cannot really understand this: which is actually my issue:

// if we assign then destructure later, it s fine
const result = constructTest(5)
const {value: {t: t1}} = result; // t: number
// --> destructuring directly make it try to create and invent the optional parameter's type
// rather than using the default one
const {value: {t: t2}} = constructTest(5); // t: any

They are basically equivalent, but do not act the same.

The overload solution may work in our case, but my real world scenario is a complex configuration object with 10+ properties.

@MartinJohns
Copy link
Contributor

MartinJohns commented Jul 12, 2022

That's a duplicate of #45074 and is fixed in TypeScript 4.8.0. It already works in the nightly version: Playground link

I found this issue using these keywords: destructure infer generic any

@incepter
Copy link
Author

Thanks a lot Martin, So all we have to do is wait for the 4.8.

@MartinJohns
Copy link
Contributor

MartinJohns commented Jul 12, 2022

wait for the 4.8.

FYI: According to #49074 the RC is scheduled for the 9th August, and the final release is scheduled for the 23th August. So a little more than a month left.

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

No branches or pull requests

2 participants