TypeScript: Error when destructuring with literal initializers as fallback

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

About this issue

  • Original URL
  • State: closed
  • Created 6 years ago
  • Reactions: 18
  • Comments: 17 (3 by maintainers)

Commits related to this issue

Most upvoted comments

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

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

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

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

https://github.com/microsoft/TypeScript/issues/26235#issuecomment-452955161

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

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.

Oops, wrong button.

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?

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