Repository navigation
Description
TypeScript Version: 2.8.0-dev.20180204
Code
declare function broke(impossible: never): never; // used to ensure full case coverage
interface Foo { kind: 'foo'; }
declare function isFoo(foobar: Foo | Bar): foobar is Foo;
interface Bar { kind: 'bar'; }
declare function isBar(foobar: Foo | Bar): foobar is Bar;
function mapFooOrBar<FooBar extends Foo | Bar, R>(
foobar: FooBar,
mapFoo: FooBar extends Foo ? ((value: Foo) => R) : ((impossible: never) => never),
mapBar: FooBar extends Bar ? ((value: Bar) => R) : ((impossible: never) => never),
): R {
if (isFoo(foobar)) {
return mapFoo(foobar);
/* ^^^^^^^^^^^^^^
Cannot invoke an expression whose type lacks a call signature. Type '((value: Foo) => R) | ((impossible: never) => never)' has no compatible call signatures. */
}
else if (isBar(foobar)) {
return mapBar(foobar);
/* ^^^^^^^^^^^^^^
Cannot invoke an expression whose type lacks a call signature. Type '((value: Bar) => R) | ((impossible: never) => never)' has no compatible call signatures. */
}
else {
return broke(foobar as never);
}
}(the casting foobar as never for the broke function is a workaround for #20375)
Expected Behavior
Compile without errors.
Actual Behavior
The types of mapFoo and mapBar are not reconsidered in light of passing the typeguard:
Cannot invoke an expression whose type lacks a call signature. Type '((value: Foo) => R) | ((impossible: never) => never)' has no compatible call signatures.
Cannot invoke an expression whose type lacks a call signature. Type '((value: Bar) => R) | ((impossible: never) => never)' has no compatible call signatures.
My use case for this is a situation where I have a union of three types, but some cases will only ever use a union of two of those types. I want this kind of utility function to be re-usable for those situations, but indicate that the handlers for unincluded cases will never be called (and allow us to type-safely stub those branches, e.g. with that broke function).
Our project uses these kinds of unions and utility functions a lot and this would significantly improve them.
Activity
[-]Suggestion: Type narrowing infers conditional types[/-][+]Suggestion: Type narrowing also narrows conditional types[/+]skeate commentedon Feb 20, 2018
I think this might be related, but it's reversed -- using a conditional mapped type to narrow the original.
krryan commentedon Feb 23, 2018
Updated this quite a bit: the example I had was poor, the
typeof"workaround" wasn't and was never going to work, but unfortunately after addressing all of that, the problem remains. It has a workaround, but type inference doesn't seem to work for the workaround (see #22149).DanielRosenwasser commentedon Mar 15, 2018
If I've read correctly, I think this is working as intended; in the general case, the type of
foobaritself doesn't necessarily reflect thatFooBar(the type variable) will describe identical types of a given instantiation. For example:krryan commentedon Mar 15, 2018
Ah right, I see what you mean. But this is a shortcoming of TypeScript as well, because the actual definitions I've used cannot cause that problem. The issue is that TS doesn't recognize that
Foo & Baris actuallyneversince thekindproperty cannot possibly be'foo'and'bar'simultaneously. If it recognized that, it could know that you will never get something that simultaneouslyisFooandisBar. That means that if you try to explicitly force the generic parameterFooBarto beFoo, eitherfoobaris aFoo(and thus notBar) and theneverinmapBaris perfectly accurate, orfoobaris aBarwhich would make theneverinmapBarwrong, except thatfoobarwouldn't then be a valid argument to the function in the first place.Some notion of that sort of mutual exclusivity also seems to be the primary impediment to #20375, which also came up in this example. If we had that, both of these problems would be tractable.
Furthermore, in this case, I don't even want
FooBaras a declared/declarable generic parameter, I would gladly usetypeof foobar—excepttypeof foobaris resolved with the declaration of the function, rather than based on the particularfoobarpassed in with any given invocation.For example, what I would have liked was
but of course,
typeof foobaris evaluated immediately asFoo | Barand so by the time I callmapFooOrBar(foo,it's already too late.Would suggestions of this mutual exclusivity notion and/or this delayed
typeofidea be worthwhile? If so, should they be separate issues, or is post itself a suggestion for them?RyanCavanaugh commentedon Mar 15, 2018
Kevin Ryan (@krryan) see #14094 for mutually exclusive types
typescript-bot commentedon Apr 12, 2018
Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.