Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Destructuring causes error for null/undefined properties #6784

Open
ghost opened this issue Feb 1, 2016 · 22 comments
Open

Destructuring causes error for null/undefined properties #6784

ghost opened this issue Feb 1, 2016 · 22 comments
Labels
Help Wanted You can do this In Discussion Not yet reached consensus Suggestion An idea for TypeScript
Milestone

Comments

@ghost
Copy link

ghost commented Feb 1, 2016

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 ) {}
@RyanCavanaugh RyanCavanaugh added By Design Deprecated - use "Working as Intended" or "Design Limitation" instead Suggestion An idea for TypeScript In Discussion Not yet reached consensus and removed By Design Deprecated - use "Working as Intended" or "Design Limitation" instead labels Feb 1, 2016
@RyanCavanaugh
Copy link
Member

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.

@ghost
Copy link
Author

ghost commented Feb 1, 2016

I agree that it should be an error if not handled elsewhere.

Destructuring assignment is a great way to clear out boilerplate code, which makes it easier to focus on domain-specific logic. It feels crippled without support for optional parameters (which are a TypeScript feature, not an ES feature).

I feel like if we extend destructuring with optional annotations - in a manner entirely consistent with conditional function parameters - it greatly enhances the utility and applicability of destructuring.

Strawman:

interface AjaxOptions {
    timeout?: number,
    onComplete?: () => void;
    onSuccess?: () => void;
    onError?: () => void;
}

type AjaxParams = { url: string, method?: string, body?: string, data: string, options?: AjaxOptions }; 

function send( { url, method = 'GET', data?, options?: { timeout, onComplete, onSuccess, onError } }: AjaxParams ) {
    // do stuff
}

The code in the body of the example can now check whether timeout is defined without worrying whether options is defined. This reduces developer effort, makes the code less error prone (especially if refactoring a property into or out of the options object), and clarifies the function by removing boilerplate code.

Basically, use the same ? syntax optional parameters use, but for properties. This cascades downward, so child properties of optional properties are themselves implicitly optional. (Or make it an error to have a non-optional descendant of an optional property.)

Using the destructuring assignment expression itself here instead of the metadata means that you must opt-in to this behavior. It is not inferred from the type definition.

@RyanCavanaugh RyanCavanaugh added Help Wanted You can do this and removed In Discussion Not yet reached consensus labels Feb 1, 2016
@RyanCavanaugh RyanCavanaugh added this to the Community milestone Feb 1, 2016
@RyanCavanaugh
Copy link
Member

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

@RyanCavanaugh RyanCavanaugh added the Effort: Difficult Good luck. label Feb 1, 2016
@Arnavion
Copy link
Contributor

Arnavion commented Feb 2, 2016

@RyanCavanaugh Just to be clear, are you accepting PRs to make { foo: { x } }: Bar (where Bar.foo is optional) an error? Or to add support for { foo?: { x } }: Bar with the emit @errorx666 is asking for?

Edit: Never mind, I see from the linked issue that it's the former.

@falsandtru
Copy link
Contributor

I already said about this problem in #6125. This issue is duplicate.

@mhegazy
Copy link
Contributor

mhegazy commented Feb 6, 2016

we are accepting PRs for flagging destructuring into a optional property as an error. We still do not want to do any type directed emit.

@jods4
Copy link

jods4 commented Mar 24, 2016

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.

@mhegazy
Copy link
Contributor

mhegazy commented Mar 24, 2016

a should be optional in this context with #2388

@jods4
Copy link

jods4 commented Mar 24, 2016

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

@TheTedAdams
Copy link

I'd be concerned about throwing a compile error when trying to destructure from an optional property.

A common case for me is destructuring a react components state object. In my state interface I have all properties marked as optional so that I can call this.setState({ onlyOnePropertyOfMany: 'value' }). But I, as the developer, know that when I retrieve the state object all the properties are there, so I like to start my render function with const { onlyOnePropertyOfMany, anotherPropertyWhichIsAnObject: { foo, bar } } = this.state;.

I also have optional properties on interfaces for objects I get from and pass to the API. I know that when I get the object it is fully populated, but when sending a PATCH I want to be able to send just the dirty fields while still having a type check that I'm not including any fields not on the object.

Even with all optional properties these interfaces provide value by making sure I spell my properties right with type checking... but this compile error would break some of my code.

@ghost
Copy link
Author

ghost commented Mar 28, 2016

@TheTedAdams I believe setState was the use-case for #4889. If the property is optional, then it can be void, and so an error can (and likely will) arise if used with destructuring without a default value. I would argue that, if the property is truly optional, then you should be forced to specify a default, and if the property is not truly optional, then it shouldn't be marked as such.
See also the suggestion for ! indicating that you have situational knowledge that an optional parameter is not void.

@RyanCavanaugh
Copy link
Member

These are good points and lead me to believe we should make this change contingent on #7355. I'll reraise for discussion.

@RyanCavanaugh RyanCavanaugh added In Discussion Not yet reached consensus and removed Help Wanted You can do this Effort: Difficult Good luck. labels Mar 28, 2016
@pauldijou
Copy link

pauldijou commented Nov 24, 2016

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)

@blakeembrey
Copy link
Contributor

blakeembrey commented Nov 26, 2016

@pauldijou Pretty sure that's unrelated to this issue (this issue is about nested destructuring that can cause a runtime error). See #9235 for index signatures.

@ghost
Copy link

ghost commented Mar 13, 2017

The sample code in this issue correctly has a compile error now. Is this resolved?

@jods4
Copy link

jods4 commented Mar 13, 2017

@andy-ms can we have a syntax to indicate something nullable is actually set and so the error should be ignored? See my previous comment #6784 (comment)

@CzBuCHi
Copy link

CzBuCHi commented Jun 5, 2017

what about doing it same way as c# ?. operator? some random blog with explanation

var property  = some?.nested?.property?.name;
// with is equivallent to:
var property = some == null ? null
    : some.nested == null ? null
        : some.nested.property == null ? null
            : some.nested.property.name;

in ts this would be current syntax

var some: { nested?: { property?: { name: string } } } = ...;
var { nested: { property: { name } } } = some; // name has type string | undefined

transpiled into:

var property = ['nested', 'property', 'name'].reduce((obj, prop) => obj && obj[prop], some);

not sure how to handle multiple nested variables ...

@danielbodart
Copy link

What's the current status of this?

@schmaluk
Copy link

schmaluk commented Apr 21, 2018

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.

@RyanCavanaugh RyanCavanaugh added the Help Wanted You can do this label Mar 7, 2019
@RyanCavanaugh RyanCavanaugh modified the milestones: Community, Backlog Mar 7, 2019
@sroze
Copy link

sroze commented Oct 13, 2019

This code will dramatically fail because destructuring an undefined value seems to be allowed:

const f1 = (something: string): { foo: string } | undefined => {
  return something == 'foo' ? { foo: 'bar' } : undefined;
};

const { foo } = f1('bar');

Shouldn't TypeScript throw an error if it's the case? Happy to put my hands in TS' code if we have agreement on this expected behaviour :)

@kitsonk
Copy link
Contributor

kitsonk commented Oct 14, 2019

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

@prajun7
Copy link

prajun7 commented Apr 13, 2022

Ran into the same problem. Any fixes?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Help Wanted You can do this In Discussion Not yet reached consensus Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests