Skip to content

Commit 8d4fe5a

Browse files
Fix unassignable properties by adding undefined with exactOptionalPropertyTypes (#45032)
* Simple first version Doesn't cover or test any complicated variations. * Lots of cases work Destructuring does not. But - skipping node_modules and lib.* does. - call expressions does - property access, including with private identifiers, does * Support variable declarations, property assignments, destructuring As long as it's not nested * More cleanup * skip all d.ts, not just node_modules/lib * Offer a codefix for a lot more cases * remove incorrect tuple check * Use getSymbolId instead of converting to string Co-authored-by: Andrew Branch <[email protected]> * add test + switch to tracking number symbol ids * Address PR comments * Exclude tuples from suggestion * Better way to get error node Plus add a check that errorNode is an argument to the call, not the call's expression. * fix semicolon lint * fix another crash * Simplify: add undefined to all optional propertie whether or not somebody tried to assign undefined to them in the erroneous assignment * remove fix-all Co-authored-by: Andrew Branch <[email protected]>
1 parent 92e7fb5 commit 8d4fe5a

31 files changed

+992
-100
lines changed

src/compiler/checker.ts

Lines changed: 49 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,7 @@ namespace ts {
635635
isTupleType,
636636
isArrayLikeType,
637637
isTypeInvalidDueToUnionDiscriminant,
638+
getExactOptionalProperties,
638639
getAllPossiblePropertiesOfTypes,
639640
getSuggestedSymbolForNonexistentProperty,
640641
getSuggestionForNonexistentProperty,
@@ -16768,24 +16769,29 @@ namespace ts {
1676816769
let sourcePropType = getIndexedAccessTypeOrUndefined(source, nameType);
1676916770
if (!sourcePropType) continue;
1677016771
const propName = getPropertyNameFromIndex(nameType, /*accessNode*/ undefined);
16771-
const targetIsOptional = !!(propName && (getPropertyOfType(target, propName) || unknownSymbol).flags & SymbolFlags.Optional);
16772-
const sourceIsOptional = !!(propName && (getPropertyOfType(source, propName) || unknownSymbol).flags & SymbolFlags.Optional);
16773-
targetPropType = removeMissingType(targetPropType, targetIsOptional);
16774-
sourcePropType = removeMissingType(sourcePropType, targetIsOptional && sourceIsOptional);
1677516772
if (!checkTypeRelatedTo(sourcePropType, targetPropType, relation, /*errorNode*/ undefined)) {
1677616773
const elaborated = next && elaborateError(next, sourcePropType, targetPropType, relation, /*headMessage*/ undefined, containingMessageChain, errorOutputContainer);
16777-
if (elaborated) {
16778-
reportedError = true;
16779-
}
16780-
else {
16774+
reportedError = true;
16775+
if (!elaborated) {
1678116776
// Issue error on the prop itself, since the prop couldn't elaborate the error
1678216777
const resultObj: { errors?: Diagnostic[] } = errorOutputContainer || {};
1678316778
// Use the expression type, if available
1678416779
const specificSource = next ? checkExpressionForMutableLocationWithContextualType(next, sourcePropType) : sourcePropType;
16785-
const result = checkTypeRelatedTo(specificSource, targetPropType, relation, prop, errorMessage, containingMessageChain, resultObj);
16786-
if (result && specificSource !== sourcePropType) {
16787-
// If for whatever reason the expression type doesn't yield an error, make sure we still issue an error on the sourcePropType
16788-
checkTypeRelatedTo(sourcePropType, targetPropType, relation, prop, errorMessage, containingMessageChain, resultObj);
16780+
if (exactOptionalPropertyTypes && isExactOptionalPropertyMismatch(specificSource, targetPropType)) {
16781+
const diag = createDiagnosticForNode(prop, Diagnostics.Type_0_is_not_assignable_to_type_1_with_exactOptionalPropertyTypes_Colon_true_Consider_adding_undefined_to_the_type_of_the_target, typeToString(specificSource), typeToString(targetPropType));
16782+
diagnostics.add(diag);
16783+
resultObj.errors = [diag];
16784+
}
16785+
else {
16786+
const targetIsOptional = !!(propName && (getPropertyOfType(target, propName) || unknownSymbol).flags & SymbolFlags.Optional);
16787+
const sourceIsOptional = !!(propName && (getPropertyOfType(source, propName) || unknownSymbol).flags & SymbolFlags.Optional);
16788+
targetPropType = removeMissingType(targetPropType, targetIsOptional);
16789+
sourcePropType = removeMissingType(sourcePropType, targetIsOptional && sourceIsOptional);
16790+
const result = checkTypeRelatedTo(specificSource, targetPropType, relation, prop, errorMessage, containingMessageChain, resultObj);
16791+
if (result && specificSource !== sourcePropType) {
16792+
// If for whatever reason the expression type doesn't yield an error, make sure we still issue an error on the sourcePropType
16793+
checkTypeRelatedTo(sourcePropType, targetPropType, relation, prop, errorMessage, containingMessageChain, resultObj);
16794+
}
1678916795
}
1679016796
if (resultObj.errors) {
1679116797
const reportedDiag = resultObj.errors[resultObj.errors.length - 1];
@@ -16813,7 +16819,6 @@ namespace ts {
1681316819
}
1681416820
}
1681516821
}
16816-
reportedError = true;
1681716822
}
1681816823
}
1681916824
}
@@ -17679,10 +17684,18 @@ namespace ts {
1767917684
else if (sourceType === targetType) {
1768017685
message = Diagnostics.Type_0_is_not_assignable_to_type_1_Two_different_types_with_this_name_exist_but_they_are_unrelated;
1768117686
}
17687+
else if (exactOptionalPropertyTypes && getExactOptionalUnassignableProperties(source, target).length) {
17688+
message = Diagnostics.Type_0_is_not_assignable_to_type_1_with_exactOptionalPropertyTypes_Colon_true_Consider_adding_undefined_to_the_types_of_the_target_s_properties;
17689+
}
1768217690
else {
1768317691
message = Diagnostics.Type_0_is_not_assignable_to_type_1;
1768417692
}
1768517693
}
17694+
else if (message === Diagnostics.Argument_of_type_0_is_not_assignable_to_parameter_of_type_1
17695+
&& exactOptionalPropertyTypes
17696+
&& getExactOptionalUnassignableProperties(source, target).length) {
17697+
message = Diagnostics.Argument_of_type_0_is_not_assignable_to_parameter_of_type_1_with_exactOptionalPropertyTypes_Colon_true_Consider_adding_undefined_to_the_types_of_the_target_s_properties;
17698+
}
1768617699

1768717700
reportError(message, generalizedSourceType, targetType);
1768817701
}
@@ -19598,6 +19611,20 @@ namespace ts {
1959819611
return isUnitType(type) || !!(type.flags & TypeFlags.TemplateLiteral);
1959919612
}
1960019613

19614+
function getExactOptionalUnassignableProperties(source: Type, target: Type) {
19615+
if (isTupleType(source) && isTupleType(target)) return emptyArray;
19616+
return getPropertiesOfType(target)
19617+
.filter(targetProp => isExactOptionalPropertyMismatch(getTypeOfPropertyOfType(source, targetProp.escapedName), getTypeOfSymbol(targetProp)));
19618+
}
19619+
19620+
function isExactOptionalPropertyMismatch(source: Type | undefined, target: Type | undefined) {
19621+
return !!source && !!target && maybeTypeOfKind(source, TypeFlags.Undefined) && !!containsMissingType(target);
19622+
}
19623+
19624+
function getExactOptionalProperties(type: Type) {
19625+
return getPropertiesOfType(type).filter(targetProp => containsMissingType(getTypeOfSymbol(targetProp)));
19626+
}
19627+
1960119628
function getBestMatchingType(source: Type, target: UnionOrIntersectionType, isRelatedTo = compareTypesAssignable) {
1960219629
return findMatchingDiscriminantType(source, target, isRelatedTo, /*skipPartial*/ true) ||
1960319630
findMatchingTypeReferenceOrTypeAliasReference(source, target) ||
@@ -32479,8 +32506,16 @@ namespace ts {
3247932506
Diagnostics.The_left_hand_side_of_an_assignment_expression_must_be_a_variable_or_a_property_access,
3248032507
Diagnostics.The_left_hand_side_of_an_assignment_expression_may_not_be_an_optional_property_access)
3248132508
&& (!isIdentifier(left) || unescapeLeadingUnderscores(left.escapedText) !== "exports")) {
32509+
32510+
let headMessage: DiagnosticMessage | undefined;
32511+
if (exactOptionalPropertyTypes && isPropertyAccessExpression(left) && maybeTypeOfKind(valueType, TypeFlags.Undefined)) {
32512+
const target = getTypeOfPropertyOfType(getTypeOfExpression(left.expression), left.name.escapedText);
32513+
if (isExactOptionalPropertyMismatch(valueType, target)) {
32514+
headMessage = Diagnostics.Type_0_is_not_assignable_to_type_1_with_exactOptionalPropertyTypes_Colon_true_Consider_adding_undefined_to_the_type_of_the_target;
32515+
}
32516+
}
3248232517
// to avoid cascading errors check assignability only if 'isReference' check succeeded and no errors were reported
32483-
checkTypeAssignableToAndOptionallyElaborate(valueType, leftType, left, right);
32518+
checkTypeAssignableToAndOptionallyElaborate(valueType, leftType, left, right, headMessage);
3248432519
}
3248532520
}
3248632521
}

src/compiler/diagnosticMessages.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1738,6 +1738,10 @@
17381738
"category": "Error",
17391739
"code": 2374
17401740
},
1741+
"Type '{0}' is not assignable to type '{1}' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties.": {
1742+
"category": "Error",
1743+
"code": 2375
1744+
},
17411745
"A 'super' call must be the first statement in the constructor when a class contains initialized properties, parameter properties, or private identifiers.": {
17421746
"category": "Error",
17431747
"code": 2376
@@ -1750,6 +1754,10 @@
17501754
"category": "Error",
17511755
"code": 2378
17521756
},
1757+
"Argument of type '{0}' is not assignable to parameter of type '{1}' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties.": {
1758+
"category": "Error",
1759+
"code": 2379
1760+
},
17531761
"The return type of a 'get' accessor must be assignable to its 'set' accessor type": {
17541762
"category": "Error",
17551763
"code": 2380
@@ -1874,6 +1882,10 @@
18741882
"category": "Error",
18751883
"code": 2410
18761884
},
1885+
"Type '{0}' is not assignable to type '{1}' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the type of the target.": {
1886+
"category": "Error",
1887+
"code": 2412
1888+
},
18771889
"Property '{0}' of type '{1}' is not assignable to '{2}' index type '{3}'.": {
18781890
"category": "Error",
18791891
"code": 2411
@@ -7118,6 +7130,10 @@
71187130
"category": "Message",
71197131
"code": 95168
71207132
},
7133+
"Add 'undefined' to optional property type": {
7134+
"category": "Message",
7135+
"code": 95169
7136+
},
71217137

71227138
"No value exists in scope for the shorthand property '{0}'. Either declare one or provide an initializer.": {
71237139
"category": "Error",

src/compiler/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4308,6 +4308,7 @@ namespace ts {
43084308
* e.g. it specifies `kind: "a"` and obj has `kind: "b"`.
43094309
*/
43104310
/* @internal */ isTypeInvalidDueToUnionDiscriminant(contextualType: Type, obj: ObjectLiteralExpression | JsxAttributes): boolean;
4311+
/* @internal */ getExactOptionalProperties(type: Type): Symbol[];
43114312
/**
43124313
* For a union, will include a property if it's defined in *any* of the member types.
43134314
* So for `{ a } | { b }`, this will include both `a` and `b`.

src/harness/fourslashImpl.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -641,7 +641,8 @@ namespace FourSlash {
641641
if (errors.length) {
642642
this.printErrorLog(/*expectErrors*/ false, errors);
643643
const error = errors[0];
644-
this.raiseError(`Found an error: ${this.formatPosition(error.file!, error.start!)}: ${error.messageText}`);
644+
const message = typeof error.messageText === "string" ? error.messageText : error.messageText.messageText;
645+
this.raiseError(`Found an error: ${this.formatPosition(error.file!, error.start!)}: ${message}`);
645646
}
646647
});
647648
}

src/services/codefixes/addMissingAwait.ts

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ namespace ts.codefix {
3232
errorCodes,
3333
getCodeActions: context => {
3434
const { sourceFile, errorCode, span, cancellationToken, program } = context;
35-
const expression = getFixableErrorSpanExpression(sourceFile, errorCode, span, cancellationToken, program);
35+
const expression = getAwaitErrorSpanExpression(sourceFile, errorCode, span, cancellationToken, program);
3636
if (!expression) {
3737
return;
3838
}
@@ -48,7 +48,7 @@ namespace ts.codefix {
4848
const checker = context.program.getTypeChecker();
4949
const fixedDeclarations = new Set<number>();
5050
return codeFixAll(context, errorCodes, (t, diagnostic) => {
51-
const expression = getFixableErrorSpanExpression(sourceFile, diagnostic.code, diagnostic, cancellationToken, program);
51+
const expression = getAwaitErrorSpanExpression(sourceFile, diagnostic.code, diagnostic, cancellationToken, program);
5252
if (!expression) {
5353
return;
5454
}
@@ -59,6 +59,13 @@ namespace ts.codefix {
5959
},
6060
});
6161

62+
function getAwaitErrorSpanExpression(sourceFile: SourceFile, errorCode: number, span: TextSpan, cancellationToken: CancellationToken, program: Program) {
63+
const expression = getFixableErrorSpanExpression(sourceFile, span);
64+
return expression
65+
&& isMissingAwaitError(sourceFile, errorCode, span, cancellationToken, program)
66+
&& isInsideAwaitableBody(expression) ? expression : undefined;
67+
}
68+
6269
function getDeclarationSiteFix(context: CodeFixContext | CodeFixAllContext, expression: Expression, errorCode: number, checker: TypeChecker, trackChanges: ContextualTrackChangesFunction, fixedDeclarations?: Set<number>) {
6370
const { sourceFile, program, cancellationToken } = context;
6471
const awaitableInitializers = findAwaitableInitializers(expression, sourceFile, cancellationToken, program, checker);
@@ -95,23 +102,6 @@ namespace ts.codefix {
95102
some(relatedInformation, related => related.code === Diagnostics.Did_you_forget_to_use_await.code));
96103
}
97104

98-
function getFixableErrorSpanExpression(sourceFile: SourceFile, errorCode: number, span: TextSpan, cancellationToken: CancellationToken, program: Program): Expression | undefined {
99-
const token = getTokenAtPosition(sourceFile, span.start);
100-
// Checker has already done work to determine that await might be possible, and has attached
101-
// related info to the node, so start by finding the expression that exactly matches up
102-
// with the diagnostic range.
103-
const expression = findAncestor(token, node => {
104-
if (node.getStart(sourceFile) < span.start || node.getEnd() > textSpanEnd(span)) {
105-
return "quit";
106-
}
107-
return isExpression(node) && textSpansEqual(span, createTextSpanFromNode(node, sourceFile));
108-
}) as Expression | undefined;
109-
110-
return expression
111-
&& isMissingAwaitError(sourceFile, errorCode, span, cancellationToken, program)
112-
&& isInsideAwaitableBody(expression) ? expression : undefined;
113-
}
114-
115105
interface AwaitableInitializer {
116106
expression: Expression;
117107
declarationSymbol: Symbol;

0 commit comments

Comments
 (0)