TypeScript: Nightly - `typeof x == "function"` does not always work as typeguard to remove a Record type from a union
Bug Report
š Search Terms
- typeof function type guard
š Version & Regression Information
- This changed between versions 4.7 and 4.8 dev
⯠Playground Link
š» Code
export function configureStore<S extends object>(
reducer: (() => void) | Record<keyof S, () => void>
) {
let rootReducer: () => void
if (typeof reducer === 'function') {
rootReducer = reducer
/*
Type '(() => void) | (Record<keyof S, () => void> & Function)' is not assignable to type '() => void'.
Type 'Record<keyof S, () => void> & Function' is not assignable to type '() => void'.
Type 'Record<keyof S, () => void> & Function' provides no match for the signature '(): void'.(2322)
*/
}
}
š Actual behavior
Here the reducer is a union between a function and a Record with a yet-unresolved key generic. In previous versions, the if (typeof reducer === 'function') { would have narrowed the type down to the function type, but now it ends up with the function type OR Record<keyof S, () => void> & Function and cannot assign the result to the variable.
š Expected behavior
No error. Our record is not a function.
About this issue
- Original URL
- State: closed
- Created 2 years ago
- Reactions: 1
- Comments: 17 (8 by maintainers)
Iām reopening this as a bug with a fix in #49422. Weāve seen similar breaking changes in other code bases and thereās really no particularly good rationale for changing behavior here.
I think I would more appreciate the answer like āweāve fixed an issue, look at those unsound thingsā than proposing those workarounds. I mean⦠itās kinda neat that you can do it but at the same time I canāt really see those as viable solutions for problems like this. Itās overly verbose and requires a deeper understanding of TS than most probably have. I also donāt know how one is supposed to even learn about patterns like this as they are only āhiddenā in the GitHub comments, SO answers etc. If this is the official answer then I think that such a recommendation should be put somewhere in the docs.
I know that you are put in a somewhat very unfortunate position - trying to balance the dynamic nature of JavaScript with correctness. Itās an almost impossible job to provide both the correct and easy-to-use language here and I appreciate all the hard work that you are doing. It feels though that such recommendations are coming from an academic perspective and they are not really pragmatic for the day-to-day use.
I just find this case to be especially surprising because it works differently from the concrete case and we can just list a lot of unsound examples that are OK for the concrete case. Like the one that you have mentioned, or like this one:
So my main question is - why is this one OK and why the reported case should error if they are both unsound? Is maintaining correctness for the reported case worth it if the 100% soundness was never TypeScriptās goal?
@weswigham Iām pretty sure every method on
Functionis already taken by a property someoneās Redux store, so like you canāt just add any new keyword we canāt just forbid a property name - but as I said, in our case itās just a nuisance šThis is definitely correct behavior, but I can see how itās annoying in this case. That said, class constructors are a really big problem here since they are also
typeof "function". SimplifiedA common suggestion is to explicitly ban functions from appearing in the non-function branch of the union:
This makes the narrowing work as expected, and prevents common function-object hybrids like classes from appearing (in non-aliasing cases)
JS in a nutshell. š¤£
Ye, I was also testing this exact case with concrete parameters yesterday and noticed the diff. It seems that I didnāt end up posting it though š
Iām also not saying that this behavior should be preserved - just saying that itās not completely incorrect here. I donāt have a good sense of where correctness and pragmatism intersect when it comes to TS because itās very much an undefined line.
The concrete case is filtered out here: https://github.dev/microsoft/TypeScript/blob/51b346d65a7d5e474a40113f72dc66106715a47a/src/compiler/checker.ts#L25179-L25181 It seems that concrete
Record<string, () => void>andFunctionare just not comparable and thusnevergets returned, so this element gets removed from the union.Itās worth noting that this happens before the generic case was filtered in
TS@<=4.7. The core of the difference is that previously the generic record was left āas isā for further processing/filtering whereas the new version creates an intersection withFunction.I myself would probably be in favor of changing the logic around this at the expense of lost ācorrectnessā here.
This is a fallout from https://github.com/microsoft/TypeScript/pull/49119
If I understand correctly⦠while annoying, this might be considered a bug fix in TS since functions might be assignable to
Records sometimes:And if we assume that this is OK (since it āalwaysā has been) then in fact the
typeof reducer === 'function'shouldnāt discard the Record-based member of the union.This doesnāt exactly match the concrete variant of the same thing but with a concrete variant we can, somewhat, see why a function is not assignable to a
Record:The error here isnāt really about a different ātypeā (function vs object) but rather about the lack of the appropriate index signature. So we can get back to our generic context example and check out what got inferred there and why a function was accepted there⦠š„
I personally know how annoying this can be - the last time I checked there was no actual way to restrict a generic to be of a POJO-like type š¢