Skip to content

Error when destructuring with literal initializers as fallback #26235

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

Closed
zombie opened this issue Aug 6, 2018 · 17 comments · Fixed by #31711
Closed

Error when destructuring with literal initializers as fallback #26235

zombie opened this issue Aug 6, 2018 · 17 comments · Fixed by #31711
Assignees
Labels
Bug A bug in TypeScript Fixed A PR has been merged for this issue

Comments

@zombie
Copy link

zombie commented Aug 6, 2018

TypeScript Version: 3.0.1

Destructuring with a fallback empty object literal is a common practice, especially in function arguments (which works), but not always:

Code

type Options = { color?: string; width?: number; };

function A({ color, width }: Options = {}) {
    //
}

function B(options: Options) {
    options = options || {};
    let { color, width } = options;
}

function C(options: Options) {
    let { color, width } = options || {};
}

Expected behavior: All three functions type-check and behave the same.

Actual behavior: "Initializer provides no value for this binding element" Error.

Which is simply incorrect, ES spec clearly defines the value when an object key is missing as undefined. Furthermore, I would argue TS should even accept let { color } = {}; though it's obviously not as common/important.

Related Issues:
Issue #4598 - Improved checking of destructuring with literal initializers fixed the same problem, but only for destructuring function arguments (example A).

Playground Link: here

@zombie zombie changed the title Error when destructuring literals as fallback Error when destructuring with literal initializers as fallback Aug 6, 2018
@ghost ghost added the Bug A bug in TypeScript label Aug 6, 2018
@ghost
Copy link

ghost commented Aug 6, 2018

const x = options || {};
let { color, width } = x;

works, so I would expect the shortened let { color, width } = options || {}; to work too.

@RyanCavanaugh
Copy link
Member

I would argue TS should even accept let { color } = {};

Should we allow let { colour } = { color: "red" } (note the spelling mismatch)? If not, what's the distinguishing principle?

@RyanCavanaugh RyanCavanaugh added this to the TypeScript 3.2 milestone Aug 8, 2018
@zombie
Copy link
Author

zombie commented Aug 9, 2018

Fair point, I concede that I was making "a principled point", while I recognize the strength of TypeScript is often in making pragmatic choices.

@zombie zombie closed this as completed Aug 9, 2018
@zombie
Copy link
Author

zombie commented Aug 9, 2018

Oops, wrong button.

@rambabusaravanan
Copy link

rambabusaravanan commented Nov 24, 2018

So can I expect Typescript 3.2.1 will not report the below snippet

let { subject, topic, subTopic } = value || {};

as below error

[ts] Initializer provides no value for this binding element and the binding element has no default value. [2525]

reference: playground

Since many es syntax are reported as error by typescript, we very restricted by the way we code 😞

@Jessidhia
Copy link

A type-safe workaround you can use is (value || {}) as Partial<NonNullable<typeof value>>.

@michaeljota
Copy link

michaeljota commented Mar 25, 2019

Is this a bug though? Typescript is correctly checking that {} as not index signature for the properties that are being destructed. In the first two examples, this works because TS have type information, and that information is correctly analysed, but in the last one, I think, Typescript is working as expecting, as {} does not have type information. The @Jessidhia answer it's not a workaround, it is telling TS the correct typing.

type Options = { color?: string; width?: number; };

function A({ color, width }: Options = {}) {
    //
}

function B(options: Options) {
    options = options || {};
    let { color, width } = options;
}

function C(options: Options) {
    let { color, width } = options || {} as Options;
}

Playground link

Better Playground link

@zombie
Copy link
Author

zombie commented Mar 25, 2019

I believe it is a bug, clearly the B example works because TS can detect that an expression options || {} is still of the type Options, which is true in the C example as well.

And while your solution is an obvious workaround in TS, this is more of a problem in type-checked Javascript (which is where I encountered it), where typing your function params and return values via JSDoc is more common, and much easier than typing individual expressions.

@zombie
Copy link
Author

zombie commented Mar 25, 2019

In fact, i believe this should have the checkJs tag.

@michaeljota
Copy link

Ok. First, as I understand this

function B(options: Options) {
    options = options || {};
    let { color, width } = options;
}

Typescript should be correctly assuming that options will never be null, or undefined, as it shouldn't be, because you are not saying that it is optional. But, if you actually mark it as optional, then you are right, I think the bug is here:

function B(options?: Options) {
    const a = options || {};
    let { color, width } = options;
}

a should be typed as Options | {}, and I guess this is getting narrow down to Options, because Options itself only have optional properties. As I understand, this the current behavior in example 2, but I think is a bug, because if you explicitly declare type OptionsOrObject = Options | {}, the type it's not being narrow down.

In example 3, and the error case, TS is checking the typing of {} as literal {} because it doesn't have type information. Casting to Options is giving that type information to TS.

Playground link

@kgregory
Copy link

kgregory commented May 17, 2019

The issue seems to present itself when destructuring from a nested object as well

interface Props {
  innerObject?: {
    name?: string;
    email?: string;
  };
}

// won't let name and email default to undefined
// Initializer provides no value for this binding element and the binding element has no default value. 
export const nestedDestructure1 = (props: Props) => {
  const { innerObject: { name, email } = {} } = props;
  console.log(name, email);
};

// no errors, but is a little annoying
export const nestedDestructure2 = (props: Props) => {
  const { innerObject: { name = undefined, email = undefined } = {} } = props;
  console.log(name, email);
};

// no errors, but is a little annoying
export const nestedDestructure3 = (props: Props) => {
  const { innerObject = {} } = props;
  const { name, email } = innerObject;
  console.log(name, email);
};

I'd really expect name and email to be typed as string | undefined in the nestedDestructure1 example, but that isn't the case.

This issue has been fixed for function params with #4598, so this would work in that situation:

export const nestedDestructure1 = ({innerObject: {name, email} = {}}: Props) => {
  console.log(name, email);
};

playground
codesandbox

@mihailik
Copy link
Contributor

Another simple obvious example how it's wrong:

type State = {
  open?: boolean;
}

class MyComponent extends React.Component<{}, State> {
  render() {
    const { open } = this.state || {};
            ~~~~
            Initializer provides no value for this binding element
            and the binding element has no default value.
            TS2525

    return <span>{open}</span>;    
  }
}

Destructuring from **** || {} is a very common pattern, and if TS can't simply fail on it.

Even if there's deep theoretical correctness reasoning how and why it's consistent with {whatever}, this pattern is common enough to have a hard-coded special casing.

@RyanCavanaugh brought up this example:

let { colour } = { color: "red" };
      ~~~~~~ Inconsistent British spelling of colour

Which basically says TypeScript helpfully highlights undeclared members for fear of typos, which is totally reasonable!

However, when destructuring from {}, there is no risk of typos. The actual error in this case protects against misspelling of a field that DOES NOT EXIST.

@ahejlsberg ahejlsberg added the Fixed A PR has been merged for this issue label Jun 2, 2019
@ahejlsberg ahejlsberg removed this from the TypeScript 3.4.0 milestone Jun 2, 2019
@ahejlsberg ahejlsberg added this to the TypeScript 3.6.0 milestone Jun 2, 2019
Haroenv added a commit to algolia/instantsearch that referenced this issue Nov 13, 2019
Although this works identical, TS will complain otherwise, see microsoft/TypeScript#26235
Haroenv added a commit to algolia/instantsearch that referenced this issue Nov 21, 2019
* chore(TS): convert geo-search util

* chore(TS): migrate prepareTemplateProps

* chore(TS): migrate src/lib/show-more

* chore: remove src/lib/show-more

apparently this isn't used!

* chore(TS): refine template type

* chore(TS): add correct default value

Although this works identical, TS will complain otherwise, see microsoft/TypeScript#26235

* chore: use already declared Template type

* chore: remove needless default value
tkrugg pushed a commit to algolia/instantsearch that referenced this issue Nov 26, 2019
* chore(TS): convert geo-search util

* chore(TS): migrate prepareTemplateProps

* chore(TS): migrate src/lib/show-more

* chore: remove src/lib/show-more

apparently this isn't used!

* chore(TS): refine template type

* chore(TS): add correct default value

Although this works identical, TS will complain otherwise, see microsoft/TypeScript#26235

* chore: use already declared Template type

* chore: remove needless default value
@hosseinmd
Copy link

I have this problem with typescript 4.

@ignacio-nacho-barbano
Copy link

ignacio-nacho-barbano commented Feb 4, 2021

#26235 (comment)

I think this is the simplest solution, did the same on my project.

@k-funk
Copy link

k-funk commented Mar 11, 2022

Ran into this when upgrading from TS 4.4 to 4.6.

const errorCode = 99
const { title, subtitle } = (() => {  // TS2525 on the deconstructed `subtitle` field
  switch (errorCode) {
    case 99:
      return {
        title: 'Account Not Found',
        subtitle: 'foo,
      }
    default:
      return {
        title: 'An unknown error occurred',
      }
  }
})()

I was able to resolve the TS2525: Initializer provides no value for this binding element and the binding element has no default value. error by explicitly returning subtitle: undefined:

    default:
      return {
        title: 'An unknown error occurred',
        subtitle: undefined,
      }

Seems like a bug to me.

@xsjcTony
Copy link

The issue seems to present itself when destructuring from a nested object as well

interface Props {
  innerObject?: {
    name?: string;
    email?: string;
  };
}

// won't let name and email default to undefined
// Initializer provides no value for this binding element and the binding element has no default value. 
export const nestedDestructure1 = (props: Props) => {
  const { innerObject: { name, email } = {} } = props;
  console.log(name, email);
};

// no errors, but is a little annoying
export const nestedDestructure2 = (props: Props) => {
  const { innerObject: { name = undefined, email = undefined } = {} } = props;
  console.log(name, email);
};

// no errors, but is a little annoying
export const nestedDestructure3 = (props: Props) => {
  const { innerObject = {} } = props;
  const { name, email } = innerObject;
  console.log(name, email);
};

I'd really expect name and email to be typed as string | undefined in the nestedDestructure1 example, but that isn't the case.

This issue has been fixed for function params with #4598, so this would work in that situation:

export const nestedDestructure1 = ({innerObject: {name, email} = {}}: Props) => {
  console.log(name, email);
};

playground codesandbox

Still an issue in v5.0.4.
Upvoting

@RyanCavanaugh
Copy link
Member

Please log a new bug if you think you have found an issue; maintainers are not necessarily reading comments on 5-year-old closed issues

@microsoft microsoft locked as resolved and limited conversation to collaborators May 12, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Bug A bug in TypeScript Fixed A PR has been merged for this issue
Projects
None yet
Development

Successfully merging a pull request may close this issue.