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
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;
}
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.
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);
};
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.
Most helpful comment
works, so I would expect the shortened
let { color, width } = options || {};
to work too.