You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Nightly bug(?) with infering function generic while being part of intersection #49307
First of all, the title exists for the sake of a title. I don't really know exactly what is wrong with this bug. I came across this while working on something with redux tookit. Error: Parameter 'state' implicitly has an 'any' type. Expected: state = WritableDraft<{username: string, isLoggedIn: true, ...}>
I am using the nightly extension and the bug has not occured because of an update to redux toolkit or a change in my code, but has instead been because of typescript versions changing.
The code itself works in typescript 4.7.0 but not in typescript nightly/4.8.0
In order to create a minimum reproducible example, I have tried to simplify it as much as possible, (which was harder than I imagined). The problem is rather weird, there are multiple parts that cause this bug and removing any will fix it. However, the issue remains, version 4.7.0 works while the same on nightly errors.
At this point, I still do not know exactly what the problem is. In fact, it may even be intentional. However, this is something that breaks redux toolkit so I think the matter is something worth taking a look at.
What search terms have I used? None. I don't know how to put this bug into words.
The PR that caused the behavior change was #48668.
Taking a closer look at Ryan Cavanaugh (@RyanCavanaugh)’s simplified repro, it’s not so clear to me what the expected behavior really should be. Both before and after #48668, T is inferred from the mapped type to be { f: unknown }, so the instantiated apparent type of reducers is
If you wanted to say that the type of the property f of that type was object & ((state: string) => void), I think that would be reasonable, but that doesn’t seem to be how we treat intersections of concrete properties and index signatures. We consistently say the type of property f of this type is just object... except in this repro pre-#48668. It looks to me like the 4.7 behavior is oddly inconsistent. Even quick info has some puzzling results:
If the contextual type of f is object, how can the contextual type of its parameter not be an implicit any?
To be clear, I don’t think the implicit any is desirable per se, but I think it’s consistent with the inference being made and the general behavior of getting types of concrete properties from an object intersected with an index signature. To get rid of the implicit any, I think we’d either need to
Infer {} for T
Make ({ [k: string]: T } & { p: U })['p'] → T & U
I don’t know what heuristic we could use to decide to do (1)—it seems super sketchy. (2) makes theoretical sense to me but would be a massive break and probably have tons of undesirable follow-on effects (I’m sure there’s an issue open about this).
TLDR, it’s not great that the behavior changed to introduce an error, but I can’t quite figure out why it ever worked before. Is there a logic to the past behavior I’m missing?
A possible rebuttal of my analysis that I thought about but forgot to address: you might say that getting a property of a contextual type is fuzzier and more permissive than getting a property of that same type via something like a property access expression. For example, if I have a union type string | { x: (state: string) => void }:
typeT=string|{x: (state: string)=>void};declareleta: T;a.x;// Error because `x` is not on all constituents of the unionconsty: T={// `state: string` - we simply ignored the unuseful constituents when// doing the "same" operation on a contextual typex(state){}}
However, if the only relevant distinction was "getting a property of a contextual type is not the same as getting a property of a type generally," we would expect this to work:
and it does not. There is something different happening not just because there is a contextual type, but because of... well, something else around that mapped type mapping over keyof T.
Andrew and I kicked this around a while and think the right move on this particular repro is Won't Fix. Reasons for this:
The thing that broke it changes other things in demonstrably good ways
In the given repro, there's just no reason to write that index signature, since it only presumably subsumes the behavior of the existing typing since all functions are objects (and in cases where this is not the case, the error is correct). The one place it doesn't is symbol keys, and if you write T in (keyof CaseReducers) & symbol]: {}, then the code works as hoped-for
We don't really know how to fix it
So anyway if you have a repro that falls under both the "intersection is necessary" and "contextual parameter inference would be sound" umbrellas, we can take a look at that in a new issue. Thanks!
Hi, I'm another Redux Toolkit maintainer. Wanted to follow up on this.
Lenz has put together a potential workaround on our side, but it requires what is frankly a pretty ugly hack (a separate package that abuses typesVersions to figure out what TS version we're dealing with, and changing what our createSlice type includes based on that).
Any thoughts on whether this issue is something the TS team intends to address before 4.8 is released?
Ryan Cavanaugh (@RyanCavanaugh) thank you! Lenz is on vacation atm, but if you've got any questions or anything, please ping me . Appreciate you at least looking at this!
Hmm, what's the current status of this particular issue? Is it considered to be a valid use case? I've found myself being somewhat confused by index signatures in more complex scenarios and I don't know how to properly reason about this.
Note that both latest reduced repros from Andrew Branch (@andrewbranch) can be fixed by using T[K] within the mapped type's template (either by ending up with it in the conditional type there or by intersecting with it): nightly TS playground
Mateusz Burzyński (@Andarist) unfortunately, the T[K] would break createSlice in older TS versions, so it's not a fix that would work for us on it's own. So we now end up adding another conditional to decide between {} and T[K] depending on the TS version.
Activity
RyanCavanaugh commentedon Jun 1, 2022
Taking possible red herrings out of the repro:
aisstringin 4.7 and implicit any in 4.8 and I don't see a good reason for that to have changed.andrewbranch commentedon Jul 6, 2022
TypeScript Bot (@typescript-bot) bisect good v4.7.4 bad main
typescript-bot commentedon Jul 6, 2022
The change between v4.7.4 and main occurred at 9236e39.
andrewbranch commentedon Jul 6, 2022
The PR that caused the behavior change was #48668.
Taking a closer look at Ryan Cavanaugh (@RyanCavanaugh)’s simplified repro, it’s not so clear to me what the expected behavior really should be. Both before and after #48668,
Tis inferred from the mapped type to be{ f: unknown }, so the instantiated apparent type ofreducersisIf you wanted to say that the type of the property
fof that type wasobject & ((state: string) => void), I think that would be reasonable, but that doesn’t seem to be how we treat intersections of concrete properties and index signatures. We consistently say the type of propertyfof this type is justobject... except in this repro pre-#48668. It looks to me like the 4.7 behavior is oddly inconsistent. Even quick info has some puzzling results:If the contextual type of
fisobject, how can the contextual type of its parameter not be an implicitany?To be clear, I don’t think the implicit
anyis desirable per se, but I think it’s consistent with the inference being made and the general behavior of getting types of concrete properties from an object intersected with an index signature. To get rid of the implicitany, I think we’d either need to{}forT({ [k: string]: T } & { p: U })['p']→T & UI don’t know what heuristic we could use to decide to do (1)—it seems super sketchy. (2) makes theoretical sense to me but would be a massive break and probably have tons of undesirable follow-on effects (I’m sure there’s an issue open about this).
TLDR, it’s not great that the behavior changed to introduce an error, but I can’t quite figure out why it ever worked before. Is there a logic to the past behavior I’m missing?
andrewbranch commentedon Jul 6, 2022
A possible rebuttal of my analysis that I thought about but forgot to address: you might say that getting a property of a contextual type is fuzzier and more permissive than getting a property of that same type via something like a property access expression. For example, if I have a union type
string | { x: (state: string) => void }:However, if the only relevant distinction was "getting a property of a contextual type is not the same as getting a property of a type generally," we would expect this to work:
and it does not. There is something different happening not just because there is a contextual type, but because of... well, something else around that mapped type mapping over
keyof T.RyanCavanaugh commentedon Jul 6, 2022
Andrew and I kicked this around a while and think the right move on this particular repro is Won't Fix. Reasons for this:
T in (keyof CaseReducers) & symbol]: {}, then the code works as hoped-forSo anyway if you have a repro that falls under both the "intersection is necessary" and "contextual parameter inference would be sound" umbrellas, we can take a look at that in a new issue. Thanks!
12 remaining items
markerikson commentedon Aug 6, 2022
Hi, I'm another Redux Toolkit maintainer. Wanted to follow up on this.
Lenz has put together a potential workaround on our side, but it requires what is frankly a pretty ugly hack (a separate package that abuses
typesVersionsto figure out what TS version we're dealing with, and changing what ourcreateSlicetype includes based on that).Any thoughts on whether this issue is something the TS team intends to address before 4.8 is released?
RyanCavanaugh commentedon Aug 11, 2022
I've been looking at
createSlicetoday to try to figure out if there's something that can be done in userland to avoid the problem.The originating use case at #48812 is a lot more compelling due to its simplicity, but I don't like breaking Peter to fix Paul.
markerikson commentedon Aug 11, 2022
Ryan Cavanaugh (@RyanCavanaugh) thank you! Lenz is on vacation atm, but if you've got any questions or anything, please ping me . Appreciate you at least looking at this!
RyanCavanaugh commentedon Aug 11, 2022
Is there a test suite or equivalent where I can try different definitions of
createSliceto make sure I'm preserving the typing of the use cases?markerikson commentedon Aug 11, 2022
Yeah. We have both unit tests, and a set of "type tests".
to run them:
Andarist commentedon Aug 11, 2022
Hmm, what's the current status of this particular issue? Is it considered to be a valid use case? I've found myself being somewhat confused by index signatures in more complex scenarios and I don't know how to properly reason about this.
Note that both latest reduced repros from Andrew Branch (@andrewbranch) can be fixed by using
T[K]within the mapped type's template (either by ending up with it in the conditional type there or by intersecting with it): nightly TS playgroundphryneas commentedon Aug 12, 2022
Mateusz Burzyński (@Andarist) unfortunately, the
T[K]would breakcreateSlicein older TS versions, so it's not a fix that would work for us on it's own. So we now end up adding another conditional to decide between{}andT[K]depending on the TS version.markerikson commentedon Aug 12, 2022
Ryan Cavanaugh (@RyanCavanaugh) : just to check, what was the final resolution? Just a reversion of the commit that caused this breakage for us?
Andarist commentedon Aug 12, 2022
Since this has been fixed by reverting the other fix - could we reopen #48812 ?
RyanCavanaugh commentedon Aug 12, 2022
Mark Erikson (@markerikson) correct - reverted in both 4.8 final and ongoing in
main. Hopefully a fix can be found that satisfies both use cases.Mateusz Burzyński (@Andarist) done 👍
markerikson commentedon Aug 12, 2022
Thank you for taking a look at this!