TypeScript: Optional chaining not working with void type
Demo
playground demo
TypeScript source with nightly version 3.8.0-dev.20191224
type Address = {
city: string
}
type User = {
home: Address | undefined
work: Address | void
}
declare let u: User
u.home?.city
u.work?.city
JavaScript output the same for both "work" and "home" properties
"use strict";
var _a, _b;
(_a = u.work) === null || _a === void 0 ? void 0 : _a.city;
(_b = u.home) === null || _b === void 0 ? void 0 : _b.city;
Bug
If you run the code above, you will see that for u.home?.city there are no errors.
But for u.work?.city there is an error:
Error: Property 'city' does not exist on type 'void | Address'
Expectation
Optional chaining works for both 'undefined' and 'void' types the same way.
About this issue
- Original URL
- State: open
- Created 5 years ago
- Reactions: 22
- Comments: 22 (9 by maintainers)
I believe my issue not about void itself. We could leave
voiddefinition the same as now. It’s aboutoptional chaining doesn't skip empty types in union type.Not working, but expected to work:
Working good:
Why
voidbreaks optional chaining - not clear for me still. What stops us from adding support for optional chaining withvoidin union type - not clear.Actually, this is a big issue for functions with optional contexts.
Currently, the only way to make a function that accepts an optional context is to define it as
this: SomeType | void, sincethis?: SomeTypedoesn’t work.Due to this issue, it’s currently impossible to use optional chaining inside the function to access properties of such context:
One could argue that this is solvable by using
undefinedinstead ofvoid, but this forces all invocations to provide the context ofundefined:So, currently, optional chaining is broken completely for functions with optional contexts. See more examples in this playground.
I, personally, have many uses for functions like this in my libraries and projects. And I believe other people have use-cases too.
I understand why both exist. What I don’t get is why typescript would ever infer a function to return void. If void means that a function could return anything, but we won’t use the value, then typescript should never infer void - how does typescript know we won’t be using the return value? It seems much more sensible for a function like
() => {}to infer as() => undefined, since typescript knows in this case that the return value is undefined, not “anything”.So I believe this one in particular should work without error as long as the code is in strict mode:
@mAAdhaTTah yeah, it can be fixed by something like:
But this is stupid and not resolves the problem.
ok, I will try to find out my root issue.
Originally, my type was
Address | undefined. For some unknown yet reasonundefinedtype getting lost through layers of generics and type inference. Therefore, in output I’ve got justAddressinstead ofAddress | undefined.Once I replaced
undefinedwithvoidmy output got much stronger andvoidnot lost. So, output became expectedAddress | void.And then, I found that I cannot use optional chaining with
voidasundefined.So let’s say you write something like this
Is this an illegal class hierarchy because callers of
Base#funcare guaranteed to get backundefined?It doesn’t seem like it is. It seems like
Basedidn’t say anything about the return type offunc, because it didn’t, andDerivedis acting in a legally covariant fashion by returning “more” than its base type.Moreover, it seems nearly pointless to talk about a function whose return value is contractually obligated to be a single particular primitive value. How many functions do you call that are specified to return
5where you consume that value? Orfalse? Or anything else? I’ve never read docs where they say “Accepts a function; this function must returntrue”. Why would anyone do that? The idea of defaulting to that mode when describing a function this way seems counter to usefulness.@Raiondesu You definitely brought up a valid case for
voidas part of the union type, but I think optional chaining support forvoidis the wrong approach to solve this (for the issues mentioned already).voiddoesn’t mean there is no context, it means there’s no type information for the context - but it might very well be present and truthy and screw over optional chaining. So even if they’d allow?.on union types involvingvoid, they couldn’t narrow the type and would have to fall back onany(or add another unsoundness to the type system).To slightly adjust your example:
thismight be truthy, but not match theFootype, and accessingbarwould cause a runtime error.To solve your issue it would be better to allow to omit the context argument when the type is
undefined.Or alternatively accept that the caller has to pass along
undefined.I think this is a simple example showing that unions with void are actually helping prevent runtime problems:
Please follow the issue template for bugs. Here you would have to fill out terms you searched for to find existing issues.
Duplicate of #35236.