admin管理员组

文章数量:1289496

Considering this very simple code:

type ExempleType = {
  someProperty?: string[];
};

const someVar: ExempleType = { someProperty: [] };

someVar.someProperty.push('test'); // => 'someVar.someProperty' is possibly 'undefined'.ts(18048)

I don't understand why typescript returns the error on this last line. someProperty cannot be undefined since it was explicitly defined.

If I slightly update the code and define someProperty just after the variable instantiation, it works:

type ExempleType = {
  someProperty?: string[];
};

const someVar: ExempleType = {};
someVar.someProperty = [];

someVar.someProperty.push('test'); // => No error!

This looks like a bug in Typescript to me... But on another end, I would expect such a bug to be caught (and fixed) very quickly, so it's probably not a bug.

So did I miss something that could explain this error?

Considering this very simple code:

type ExempleType = {
  someProperty?: string[];
};

const someVar: ExempleType = { someProperty: [] };

someVar.someProperty.push('test'); // => 'someVar.someProperty' is possibly 'undefined'.ts(18048)

I don't understand why typescript returns the error on this last line. someProperty cannot be undefined since it was explicitly defined.

If I slightly update the code and define someProperty just after the variable instantiation, it works:

type ExempleType = {
  someProperty?: string[];
};

const someVar: ExempleType = {};
someVar.someProperty = [];

someVar.someProperty.push('test'); // => No error!

This looks like a bug in Typescript to me... But on another end, I would expect such a bug to be caught (and fixed) very quickly, so it's probably not a bug.

So did I miss something that could explain this error?

Share Improve this question edited Feb 19 at 19:33 VLAZ 29.1k9 gold badges62 silver badges84 bronze badges asked Feb 19 at 18:00 ZoddoZoddo 2261 silver badge11 bronze badges 6
  • 1 "I don't understand why typescript returns the error on this last line. someProperty cannot be undefined since it was explicitly defined." but it can be undefined based on the type you've defined. If you really want to say that it can never be undefined, then don't make it optional. If you want it optional, then be prepared to handle the case when it's missing. You can't have both. – VLAZ Commented Feb 19 at 18:12
  • This is a simplified example. Consider that ExampleType comes from a library you cannot modify (because the object you construct is intended to be passed to a method of this lib, and it's effectively optional to provide this property). I understand the typing says it can be undefined, but TypeScript should infer that it's not undefined in this case (like it does in the second example). – Zoddo Commented Feb 19 at 18:16
  • 2 Not a bug: you annotated someVar with type ExempleType, so that's all TS knows about it. Specific info from the initializer is lost. TS does have assignment narrowing, but that only works on unions. ExempleType is not a union, so assigning to it doesn't narrow. But ExempleType["someProperty"] is effectively a union, string[] | undefined, so assigning does narrow (and I think optional prop assignments narrow). Does that fully address the q? If so I'll write an a or find a duplicate; if not, what's missing? – jcalz Commented Feb 19 at 18:28
  • "Specific info from the initializer is lost." -> Ok, this explains the issue. So it's not a bug, but more an unsupported scenario in type narrowing. So in such a case, I believe there is no way to keep the info from the initialization? – Zoddo Commented Feb 19 at 18:45
  • Don't annotate the type to start with? github/microsoft/TypeScript/issues/51853 is relevant; you might do this to convey your intent, but it's wordy. This feels out of scope for the question as asked, though... is it okay if I don't worry about this part here? – jcalz Commented Feb 19 at 19:21
 |  Show 1 more comment

2 Answers 2

Reset to default 1

If you annotate a variable with a (non-union) type like ExempleType, then that's all TypeScript knows about the type of that variable. Any more specific information about the type of a value you assign to it is simply not tracked. There is such a thing as assignment narrowing, but that only works for union types. So if you write let x: string[] | undefined = [], then x is narrowed to string[] after that assignment. But the similar-looking let y: { x: string[] | undefined } = { x: [] } doesn't work the same way because you are assigning to a non-union object type whose property happens to be a union. It would be nice if assignment narrowing worked on non-union types, also, but it doesn't happen (see this comment on microsoft/TypeScript#8513 for more information).

This explains the difference between const someVar: ExempleType = { someProperty: [] }; and const someVar: ExempleType = {}; someVar.someProperty = [];. In the latter case you have assigned the property, which is effectively a union type (optional properties behave similarly to unions with undefined) so it is narrowed to string[]. Subsequent direct property accesses like someVar.someProperty will then see string[], but someVar itself does not get narrowed:

const someVar: ExempleType = {};
someVar.someProperty = [];
someVar.someProperty.push('test'); // no error
function foo(r: Required<ExempleType>) { }
foo(someVar); // error!  Types of property 'someProperty' are incompatible.

Generally speaking if you want TypeScript to keep track of specific information from initializers (and you're not planning to do a lot of mutation) then you might want to avoid annotating entirely, and use the satisfies operator to check that the initializer is compatible with that type.

This is a little messy with empty arrays, because [] will end up being inferred as never[], even with satisfies. There's an open issue at microsoft/TypeScript#51853. Right now the "safe" way that uses satisfies is wordy:

const someVar = {
    someProperty: [] satisfies string[] as string[]
} satisfies ExempleType;

/* const someVar: { someProperty: string[]; } */

The expr satisfies T as T construction effectively first checks that expr is assignable to T and then widens it to T, so [] satisfies string[] as string[] ends up with [] as type string[] after checking that it works. It's easier to just write [] as string[] but that's a little less safe. And then the satisfies at the end makes sure that { someProperty: string[] } is compatible with ExempleType, which it is... but someVar is now narrowed than ExempleType; it's someProperty is known to be string[], and the rest of your code works as desired:

someVar.someProperty.push('test'); // okay
function foo(r: Required<ExempleType>) { }
foo(someVar); // okay

Playground link to code

The reason Typescript tells you the property could be undefined is because that is what you tell it. When you write const x: T = y, Typescript treats x as T, regardless of the actual value of y (though y must be assignable to T).

If you really want Typescript to use the type of y for x, you can use const x = y satisfies T, which tells Typescript to check that y is assignable to T without casting y to T.

本文标签: typescriptproperty is possibly 39undefined39 while it39s been explicitly definedStack Overflow