Typescript: Error when destructuring with literal initializers as fallback

Created on 6 Aug 2018  路  13Comments  路  Source: microsoft/TypeScript

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

Bug Fixed

Most helpful comment

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

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

All 13 comments

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

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

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.

Oops, wrong button.

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 馃槥

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

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

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.

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

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

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

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.

I have this problem with typescript 4.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

siddjain picture siddjain  路  3Comments

blendsdk picture blendsdk  路  3Comments

wmaurer picture wmaurer  路  3Comments

jbondc picture jbondc  路  3Comments

Zlatkovsky picture Zlatkovsky  路  3Comments