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)

Most upvoted comments

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.

Not working, but expected to work:

type Address = {
    city: string
}

declare let value: Address | void | null | undefined | never

value?.city

Working good:

declare let value: Address | null | undefined | never

value?.city

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.

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:

type Foo = { bar?: { baz: string; }; };

function example(this: Foo | void, value: string) {
  return this?.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:

type Foo = { bar?: { baz: string; }; };

function example(this: Foo | undefined, value: string) {
  return this?.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.

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:

function exampleUndefined(this: Foo | undefined, value: string) {
  console.log(this);
  // Works here
  return this?.bar?.baz;
}

// Wrongly complains here!
// Valid JS code, though.
exampleUndefined('default string'); // logs 'undefined'

@mAAdhaTTah yeah, it can be fixed by something like:

.catch((e) => { console.warn(e); return undefined; })

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 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.

So let’s say you write something like this

class Base {
  func() {  }
}
class Derived extends Base {
  func() {
    return 0;
  }
}

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.

@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 think this is a simple example showing that unions with void are actually helping prevent runtime problems:

function simpleCallback() { }

async function advancedCallback() {
  await Promise.resolve();
}

// registerCallback can be called using simpleCallback or advancedCallback for example
function registerCallback(callback: () => Promise<any> | void) {
  // Does not compile unfortunately:
  // callback()?.catch(console.error);
  
  // workaround without optional chaining
  let result = callback();
  if (result) {
    result.catch(console.error)
  }
}

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.