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
Optional chaining not working with void type #35850
I believe my issue not about void itself. We could leave void definition the same as now.
It's about optional chaining doesn't skip empty types in union type.
Why void breaks optional chaining - not clear for me still.
What stops us from adding support for optional chaining with void in union type - not clear.
Reacted by Alexey Iskhakov, César Guadarrama, Dmitrii Kanatnikov, Fabio Spampinato, Daniel, Nishant Dani, Karol Pacheco, elishevarom, Bence Szabó, Lars Grammel and 2 more
Your issue is about your type being potentially void. Please check the other issue I linked you, the one you duplicated. Ryan provided reasoning there.
Originally, my type was Address | undefined.
For some unknown yet reason undefined type getting lost through layers of generics and type inference.
Therefore, in output I've got just Address instead of Address | undefined.
Once I replaced undefined with void my output got much stronger and void not lost.
So, output became expected Address | void.
And then, I found that I cannot use optional chaining with void as undefined.
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, since this?: SomeType doesn't work.
Due to this issue, it's currently impossible to use optional chaining inside the function to access properties of such context:
typeFoo={bar?: {baz: string;};};functionexample(this: Foo|void,value: string){returnthis?.bar?.baz??value;// Currently an error here}example('default string');
One could argue that this is solvable by using undefined instead of void, but this forces all invocations to provide the context of undefined:
typeFoo={bar?: {baz: string;};};functionexample(this: Foo|undefined,value: string){returnthis?.bar?.baz??value;// No error, cool}example('default string');// "The 'this' context of type 'void' is not assignable to method's 'this' of type 'Foo | undefined'." 😕
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.
Reacted by sullivan-ben, Karol Pacheco, Emmanuel Ogbizi and Lars Grammel
I think this is a simple example showing that unions with void are actually helping prevent runtime problems:
functionsimpleCallback(){}asyncfunctionadvancedCallback(){awaitPromise.resolve();}// registerCallback can be called using simpleCallback or advancedCallback for examplefunctionregisterCallback(callback: ()=>Promise<any>|void){// Does not compile unfortunately:// callback()?.catch(console.error);// workaround without optional chainingletresult=callback();if(result){result.catch(console.error)}}
Johan Gorter (@johan-gorter) I think you should rather show why void should be part of the union type, and not undefined.
void in TypeScript means the absence of a type, not the absence of a value. There might be a value, but it shouldn't be used. If a value is present your check might even produce false-positive.
Martin Johns (@MartinJohns), there's no arguing that void shouldn't be a part of a union type that defines a value.
But there is a case I mentioned earlier in which there's no way for void not to be a part of such a type, as it just breaks typescript altogether.
In Johan Gorter (@johan-gorter)'s example, the void type can be easily replaced with undefined - voilà, the problem is solved, optional chaining works:
functionsimpleCallback(): undefined{return;}asyncfunctionadvancedCallback(){awaitPromise.resolve();}// registerCallback can be called using simpleCallback or advancedCallback for examplefunctionregisterCallback(callback: ()=>Promise<any>|undefined){// EZcallback()?.catch(console.error);}
However, in the case of void being the part of a union type for a function context (this) - it's impossible to just remove the void or replace it with undefined.
Alexey Iskhakov (@Raiondesu) You definitely brought up a valid case for void as part of the union type, but I think optional chaining support for void is the wrong approach to solve this (for the issues mentioned already).
void doesn'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 involving void, they couldn't narrow the type and would have to fall back on any (or add another unsoundness to the type system).
To slightly adjust your example:
// `bar` is not optional anymore.
type Foo = { bar: { baz: string; }; };
function example(this: Foo | void, value: string) {
// Only use optional chaining for `this`:
return this?.bar.baz ?? value;
}
this might be truthy, but not match the Foo type, and accessing bar would cause a runtime error.
To solve your issue it would be better to allow to omit the context argument when the type is undefined.
type Foo = { bar?: { baz: string; }; };
function example(this: Foo | undefined, value: string) { return this?.bar?.baz ?? value; }
// Make this legal!
example('default string');
Or alternatively accept that the caller has to pass along undefined.
I now clearly see the problem with this. I do agree with you.
But I still don't see how the context could just "suddenly" be different from the expected one (assuming we run code in 'use strict' mode).
I mean, unless the user explicitly sets the context using call, bind or apply, the context is undefined by-default. This means that TypeScript would intervene whenever the user changes context to the one that doesn't match the type.
And, also, if (this) still works, so this doesn't really add up with the "this might be truthy" argument. Why is optional chaining outlawed in this case, but a simple if type-guard is not?
If what you're implying is indeed true, and void represents that "no type information" is given - then we should've also outlawed if (this) here along with optional chaining on this (which would break the backward compatibility). Or... we could just allow optional chaining, as it is just a shortcut for if (this), is it not?
To solve your issue it would be better to allow to omit the context argument when the type is undefined.
This is not an intended use of void and you should never, ever on purpose write something like Foo | void in an input position. Any time you're tempted to write void like that, use unknown, never, or undefined depending on what you mean.
I get it, Ryan Cavanaugh (@RyanCavanaugh), thanks, but how to deal with the function's context then?
It can't be never, undefined nor unknown. All three break the valid JS code of just calling the function afterwards without specifying the context.
Here are the examples in playground.
All examples listed are broken in some way or another. Even though two of them (void and undefined) are absolutely correct for the circumstance (in my opinion), TS doesn't allow any. Doesn't this mean that at least one of those types is broken in some way?
Not sure if this is the right issue to leave this, but I think inference of void is a problem, as a function that does not return anything is inferred as () => void, and cannot be assigned to () => undefined, even though in this case TypeScript should know that my function actually returns undefined. See this playground for an example.
I'm also not sure why || works in place of ?? when void is involved.
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".
Reacted by Emile J. Bergeron, Casey Occhialini and jwalkerinterpres
Is this an illegal class hierarchy because callers of Base#func are guaranteed to get back undefined ?
It doesn't seem like it is. It seems like Base didn't say anything about the return type of func, because it didn't, and Derived is 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 5 where you consume that value? Or false ? Or anything else? I've never read docs where they say "Accepts a function; this function must return true". Why would anyone do that? The idea of defaulting to that mode when describing a function this way seems counter to usefulness.
Activity
MartinJohns commentedon Dec 25, 2019
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.
DanielRosenwasser commentedon Dec 26, 2019
voidis meant to be treated as an opaque return type for functions, since the type system allows you to assign() => numberto a() => void.In other words, it signals not just "this function doesn't return anything", but also "this functions values are not meant to be witnessed".
overshom commentedon Dec 26, 2019
I believe my issue not about void itself. We could leave
voiddefinition the same as now.It's about
optional 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 with
voidin union type - not clear.MartinJohns commentedon Dec 26, 2019
Your issue is about your type being potentially
void. Please check the other issue I linked you, the one you duplicated. Ryan provided reasoning there.overshom commentedon Dec 26, 2019
ok, I will try to find out my root issue.
Originally, my type was
Address | undefined.For some unknown yet reason
undefinedtype getting lost through layers of generics and type inference.Therefore, in output I've got just
Addressinstead ofAddress | undefined.Once I replaced
undefinedwithvoidmy output got much stronger andvoidnot lost.So, output became expected
Address | void.And then, I found that I cannot use optional chaining with
voidasundefined.Raiondesu commentedon Dec 27, 2019
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.
johan-gorter commentedon Jan 3, 2020
I think this is a simple example showing that unions with void are actually helping prevent runtime problems:
MartinJohns commentedon Jan 3, 2020
Johan Gorter (@johan-gorter) I think you should rather show why
voidshould be part of the union type, and notundefined.voidin TypeScript means the absence of a type, not the absence of a value. There might be a value, but it shouldn't be used. If a value is present your check might even produce false-positive.http://www.typescriptlang.org/docs/handbook/basic-types.html#void
https://github.com/microsoft/TypeScript/wiki/FAQ#why-are-functions-returning-non-void-assignable-to-function-returning-void
See also this comment by Ryan Cavanaugh (@RyanCavanaugh):
Raiondesu commentedon Jan 3, 2020
Martin Johns (@MartinJohns), there's no arguing that
voidshouldn't be a part of a union type that defines a value.But there is a case I mentioned earlier in which there's no way for
voidnot to be a part of such a type, as it just breaks typescript altogether.In Johan Gorter (@johan-gorter)'s example, the
voidtype can be easily replaced withundefined- voilà, the problem is solved, optional chaining works:However, in the case of
voidbeing the part of a union type for a function context (this) - it's impossible to just remove thevoidor replace it withundefined.MartinJohns commentedon Jan 3, 2020
Alexey Iskhakov (@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.Raiondesu commentedon Jan 3, 2020
I now clearly see the problem with this. I do agree with you.
But I still don't see how the context could just "suddenly" be different from the expected one (assuming we run code in
'use strict'mode).I mean, unless the user explicitly sets the context using
call,bindorapply, the context isundefinedby-default. This means that TypeScript would intervene whenever the user changes context to the one that doesn't match the type.And, also,
if (this)still works, so this doesn't really add up with the "thismight be truthy" argument. Why is optional chaining outlawed in this case, but a simpleiftype-guard is not?If what you're implying is indeed true, and
voidrepresents that "no type information" is given - then we should've also outlawedif (this)here along with optional chaining onthis(which would break the backward compatibility). Or... we could just allow optional chaining, as it is just a shortcut forif (this), is it not?Should I create a separate issue then?
RyanCavanaugh commentedon Jan 14, 2020
This is not an intended use of
voidand you should never, ever on purpose write something likeFoo | voidin an input position. Any time you're tempted to writevoidlike that, useunknown,never, orundefineddepending on what you mean.Raiondesu commentedon Jan 15, 2020
I get it, Ryan Cavanaugh (@RyanCavanaugh), thanks, but how to deal with the function's context then?
It can't be
never,undefinednorunknown. All three break the valid JS code of just calling the function afterwards without specifying the context.Here are the examples in playground.
All examples listed are broken in some way or another. Even though two of them (
voidandundefined) are absolutely correct for the circumstance (in my opinion), TS doesn't allow any. Doesn't this mean that at least one of those types is broken in some way?RyanCavanaugh commentedon Jan 15, 2020
So I believe this one in particular should work without error as long as the code is in strict mode:
ne-spal commentedon Jan 24, 2020
Ryan Cavanaugh (@RyanCavanaugh) but one more advantage of using
voidis it cannot be dropped by!bschlenk commentedon Oct 1, 2020
Not sure if this is the right issue to leave this, but I think inference of void is a problem, as a function that does not return anything is inferred as
() => void, and cannot be assigned to() => undefined, even though in this case TypeScript should know that my function actually returnsundefined. See this playground for an example.I'm also not sure why
||works in place of??when void is involved.RyanCavanaugh commentedon Oct 1, 2020
Brian Schlenker (@bschlenk) see https://stackoverflow.com/questions/58885485/why-does-typescript-have-both-void-and-undefined
bschlenk commentedon Oct 1, 2020
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".RyanCavanaugh commentedon Oct 2, 2020
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.HanabishiRecca commentedon Apr 7, 2021
I have faced this in such situation:
Playground
I'm not using
voidanywhere in my code, butPromise.catchitself returnsvoid | Twhich is very annoying.mAAdhaTTah commentedon Apr 7, 2021
Hanabishi (@HanabishiRecca) The problem isn't
catchbutconsole.warn, which returnsvoid.HanabishiRecca commentedon Apr 7, 2021
James DiGioia (@mAAdhaTTah) yeah, it can be fixed by something like:
But this is stupid and not resolves the problem.