TypeScript: Destructuring causes error for null/undefined properties

The following code:

interface Foo {
    x: number;
}

interface Bar {
    foo?: Foo;
}

function test( { foo: { x } }: Bar ) {
    alert( x );
}

test( {} ); // TypeError: Cannot read property 'x' of undefined

transpiles to:

function test(_a) {
    var x = _a.foo.x;
    alert(x);
}

instead of the (arguably) preferable:

function test(_a) {
    var x;
    if (_a.foo) x = _a.foo.x;
    alert(x);
}

The transpiled code does not check for the presence of optional property foo in the Bar interface. This leads to an error at invocation.

I can see three possible solutions:

  1. Always check for member existence when transpiling destructuring statements.
  2. Check for member existence only when an element is optional in the interface or type declaration.
  3. New syntax for destructuring that can indicate members should be checked.

E.g.

function test( { foo?: { x } }: Bar ) {}

About this issue

  • Original URL
  • State: open
  • Created 8 years ago
  • Reactions: 10
  • Comments: 22 (9 by maintainers)

Most upvoted comments

What’s the current status of this?

I think you also have the problem when destructuring arrays.

const [a, b, c] = ''.split('')

TypeScript will consider all three variables to be string while they should be string | undefined (because, they are totally undefined here)

const [a, b] : string[] = [];
const d: string = b.toUpperCase();

In strict-mode should be invalid. Why only at runtime I get aTypeError: Cannot read property ‘toUpperCase’ of undefined ?

Thought in strict-mode it is guaranteed that values are not null or undefined implicitly 😦(( I think it would be great if this can be fixed, A proper destructuring should handle all cases recursively like an “empty array” somehow… Maybe by recursion.

This is just the ES6 destructuring behavior; we don’t use type system data to change the runtime behavior.

That said, it’s a bad idea to use a property of an optional property as part of a destructuring, and we should probably consider this an error.

@mhegazy Yes but this was just for illustration. You could know that an optional member is not null because of some domain-specific knowledge. In fact that’s the whole purpose of x!.

So if we forbid destructing an optional member, should we be able to do this?

let { a!: { b } } = x;

Thinking about it, I guess it makes sense. TS is about preventing errors and destructuring from null or undefined throws a runtime error so it should be caught by TS. With the new work on null types and data flow analysis, one should be allowed to destructure an optional member that’s proven to be present. And if the program logic implies it is present I should be able to indicate this to TS with a bang.

This implies that in cases where I know (because of program context) that the optional property is present in my object, I won’t be able to destructure? Even though that would be valid, working JS?

interface X { a?: { b: number } };
let x: X;

if (x.a) { // or some other domain rule that guarantees me 'a' is defined
  let { a: { b } } = x; // error: "a" is optional ?
}

I don’t have any use for such code, just pointing out the possible implications if it becomes an error.

@sroze it does error if you have strict null checks on. When you run in sloppy mode, it behaves as expected and does not error.

Accepting PRs. Any property destructuring must be non-optional or have a default