Skip to content

Unstable type inference when generic is inferred from a return type #29771

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
Jessidhia opened this issue Feb 6, 2019 · 6 comments
Closed

Unstable type inference when generic is inferred from a return type #29771

Jessidhia opened this issue Feb 6, 2019 · 6 comments
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed

Comments

@Jessidhia
Copy link

Jessidhia commented Feb 6, 2019

TypeScript Version: 3.2.4, 3.3.1, 3.4.0-dev.20190206

Search Terms: generic return

Code

declare function useDerivedState<T extends unknown>(
  initializer: (previous: T | undefined) => T
): T

function makeFoo(previous: { foo?: string } = {}) {
  return {
    ...previous,
    foo: 'bar'
  }
}

const state = useDerivedState(previous => makeFoo(previous))
state.foo

Expected behavior:

foo exists and is accessible

Actual behavior:

Very inconsistent.

This image is probably the most egregious example (state is { foo: string }):

image

But if I then type a whitespace or something to get the compiler to update, sometimes it starts seeing state as {}:

image

There are also some cases I can't screenshot properly. For example, this is the full derivation when it is almost working correctly:

image

But if I cause some input to happen, the derivation might reset to <{}>. However, pressing command to be able to use the screenshot tool causes the tooltip to update and it goes back to the full derivation.

The only thing that is consistent is that type of the previous argument seems to always be derived as {}.

Related Issues: #29638 and probably a bunch of others that look similar but don't quite show this odd / pathological behavior.


Giving makeFoo directly as the argument to the function avoids the problem but the current state is a minimum reproducible example of a problem I found that can't just be changed to that.

@dragomirtitian
Copy link
Contributor

A samller example of what you are trying to do would be:

declare function useDerivedState<T>(
  initializer: (previous: T ) => T
): T

const state = useDerivedState(previous => 0) // state is {}

Just as an opinion, I don't think typescript can deal with this scenario well at the moment. It's probably trying to figure out the type of the callback, and to do that it needs a type for the parameter, and there is nowhere to get that from so goes with the default of {}.

@Jessidhia
Copy link
Author

Jessidhia commented Feb 6, 2019

Yeah, that is probably the central cause, but the odd thing is that the inference was so unstable and would change by just interacting with the IDE even without changing the source code.


Luckily, because I only actually needed a small portion of the previous that has a very simple type to be able to calculate the next value, I managed to refactor things such that I can use ReturnType to get the fairly complex derived type of previous itself.

useDerivedState(
  (previous: ReturnType<typeof deriveFoo>) =>
    deriveFoo(previous !== undefined ? previous.foo.bar : undefined)
)

function deriveFoo(argument?: BarType) {
  return fooFactory(argument, factoryOptions)
}

@RyanCavanaugh RyanCavanaugh added the Design Limitation Constraints of the existing architecture prevent this from being fixed label Feb 14, 2019
@RyanCavanaugh
Copy link
Member

Our inference system can't really deal with a t => t' kind of function to do inferences from both sides to figure out what's going on. Sometimes language service operations manage to avoid noticing, but this is really a side effect.

@Weakky
Copy link

Weakky commented Mar 11, 2019

Same here, which is very sad 😞

interface ServerConfig<T, U, V> {
  server: () => T
  startServer: (param: T) => U
  stopServer: (param: U) => V
}

function config<T, U, V>(cfg: ServerConfig<T, U, V>): ServerConfig<T, U, V> {
  return cfg
}

image

Expected behavior:

ServerConfig to be of type

ServerConfig<{ foo:'bar' }, string, number>

Actual behavior:

ServerConfig<{ foo:'bar' }, {}, number>

Playgroung link

The return types are sometimes inferred, sometimes not, which makes it unreliable 😢

@MicahZoltu
Copy link
Contributor

Oddly, when you give the compiler less information it gets the inference correct. I don't know why this is, and perhaps is related to the "unreliability" that others were talking about:

interface Apple<T> {
  parse: (value: string) => T,
  stringify: (value: T) => string,
};
declare function eatApple<T>(apple: Apple<T>): void
eatApple({
    // infers `T` as `unknown`
    parse: value => Number.parseInt(value),
    // errors because `value` is `unknown`
    stringify: value => value.toString(),
})
eatApple({
    // infers `T` as `number`
    parse: (value: string) => Number.parseInt(value),
    // works because `value` is `number`
    stringify: value => value.toString(),
})

interface Banana<T> {
    parse: (value: string) => T
}
declare function eatBanana<T>(banana: Banana<T>): void
eatBanana({
    // `T` correctly inferred as `number`
    parse: value => Number.parseInt(value),
})

Of note is that an instance of Apple incorrectly inferred T as unknown when given an untyped parse function, however Banana correctly inferred T as number when given the exact same function. The only difference between the two is that Apple has additional properties.

@threehams
Copy link

That makes sense, though. Inference only breaks down when the generic is used in both a return type position and another position.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed
Projects
None yet
Development

No branches or pull requests

6 participants