Skip to content

Commit 664d749

Browse files
committed
Simple first version
Doesn't cover or test any complicated variations.
1 parent 8e79510 commit 664d749

File tree

6 files changed

+140
-2
lines changed

6 files changed

+140
-2
lines changed

src/compiler/checker.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -633,6 +633,7 @@ namespace ts {
633633
isTupleType,
634634
isArrayLikeType,
635635
isTypeInvalidDueToUnionDiscriminant,
636+
getExactOptionalUnassignableProperties,
636637
getAllPossiblePropertiesOfTypes,
637638
getSuggestedSymbolForNonexistentProperty,
638639
getSuggestionForNonexistentProperty,
@@ -17873,6 +17874,7 @@ namespace ts {
1787317874

1787417875
function reportErrorResults(source: Type, target: Type, result: Ternary, isComparingJsxAttributes: boolean) {
1787517876
if (!result && reportErrors) {
17877+
let message = headMessage
1787617878
const sourceHasBase = !!getSingleBaseForNonAugmentingSubtype(originalSource);
1787717879
const targetHasBase = !!getSingleBaseForNonAugmentingSubtype(originalTarget);
1787817880
source = (originalSource.aliasSymbol || sourceHasBase) ? originalSource : source;
@@ -17904,15 +17906,18 @@ namespace ts {
1790417906
return result;
1790517907
}
1790617908
}
17909+
else if (exactOptionalPropertyTypes && getExactOptionalUnassignableProperties(source, target).length) {
17910+
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
17911+
}
1790717912
else {
1790817913
errorInfo = elaborateNeverIntersection(errorInfo, originalTarget);
1790917914
}
17910-
if (!headMessage && maybeSuppress) {
17915+
if (!message && maybeSuppress) {
1791117916
lastSkippedInfo = [source, target];
1791217917
// Used by, eg, missing property checking to replace the top-level message with a more informative one
1791317918
return result;
1791417919
}
17915-
reportRelationError(headMessage, source, target);
17920+
reportRelationError(message, source, target);
1791617921
}
1791717922
}
1791817923
}
@@ -19574,6 +19579,16 @@ namespace ts {
1957419579
return isUnitType(type) || !!(type.flags & TypeFlags.TemplateLiteral);
1957519580
}
1957619581

19582+
// TODO: Only issue with source has undefined, target does not, and they are otherwise assignable/the same (or who cares)
19583+
function getExactOptionalUnassignableProperties(source: Type, target: Type) {
19584+
return checker.getPropertiesOfType(target).filter(targetProp => {
19585+
const sourceProp = getPropertyOfType(source, targetProp.escapedName)
19586+
return sourceProp && targetProp.valueDeclaration
19587+
&& maybeTypeOfKind(getTypeOfSymbol(sourceProp), TypeFlags.Undefined)
19588+
&& hasQuestionToken(targetProp.valueDeclaration)
19589+
&& containsMissingType(getTypeOfSymbol(targetProp)) })
19590+
}
19591+
1957719592
function getBestMatchingType(source: Type, target: UnionOrIntersectionType, isRelatedTo = compareTypesAssignable) {
1957819593
return findMatchingDiscriminantType(source, target, isRelatedTo, /*skipPartial*/ true) ||
1957919594
findMatchingTypeReferenceOrTypeAliasReference(source, target) ||

src/compiler/diagnosticMessages.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1698,6 +1698,10 @@
16981698
"category": "Error",
16991699
"code": 2374
17001700
},
1701+
"Type '{0}' is not assignable to type '{1}' with exactOptionalPropertyTypes: true. Consider adding 'undefined' to the types of the target's properties.": {
1702+
"category": "Error",
1703+
"code": 2375
1704+
},
17011705
"A 'super' call must be the first statement in the constructor when a class contains initialized properties, parameter properties, or private identifiers.": {
17021706
"category": "Error",
17031707
"code": 2376
@@ -7058,6 +7062,14 @@
70587062
"category": "Message",
70597063
"code": 95166
70607064
},
7065+
"Add 'undefined' to all optional properties": {
7066+
"category": "Message",
7067+
"code": 95167
7068+
},
7069+
"Add 'undefined' to optional property type": {
7070+
"category": "Message",
7071+
"code": 95168
7072+
},
70617073

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

src/compiler/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4301,6 +4301,7 @@ namespace ts {
43014301
* e.g. it specifies `kind: "a"` and obj has `kind: "b"`.
43024302
*/
43034303
/* @internal */ isTypeInvalidDueToUnionDiscriminant(contextualType: Type, obj: ObjectLiteralExpression | JsxAttributes): boolean;
4304+
/* @internal */ getExactOptionalUnassignableProperties(source: Type, target: Type): Symbol[];
43044305
/**
43054306
* For a union, will include a property if it's defined in *any* of the member types.
43064307
* So for `{ a } | { b }`, this will include both `a` and `b`.
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/* @internal */
2+
namespace ts.codefix {
3+
const addOptionalPropertyUndefined = "addOptionalPropertyUndefined";
4+
5+
const errorCodes = [
6+
Diagnostics.Type_0_is_not_assignable_to_type_1_with_exactOptionalPropertyTypes_Colon_true_Consider_adding_undefined_to_the_types_of_the_target_s_properties.code
7+
];
8+
9+
registerCodeFix({
10+
errorCodes,
11+
getCodeActions(context) {
12+
const typeChecker = context.program.getTypeChecker();
13+
const info = getInfo(context.sourceFile, context.span.start, typeChecker);
14+
if (!info.length) {
15+
return undefined;
16+
}
17+
// if method, it has to be rewritten to property
18+
// skip any and unions with any
19+
// add to existing unions
20+
// parenthesise conditional types and arrows (the printer should take care of that, but it needs a test)
21+
// test with destructuring, I've no idea what to do there
22+
const changes = textChanges.ChangeTracker.with(context, t => addUndefinedToOptionalProperty(t, info));
23+
return [createCodeFixAction(addOptionalPropertyUndefined, changes, Diagnostics.Add_undefined_to_optional_property_type, addOptionalPropertyUndefined, Diagnostics.Add_undefined_to_all_optional_properties)];
24+
},
25+
fixIds: [addOptionalPropertyUndefined],
26+
getAllCodeActions: context => {
27+
const { program } = context;
28+
const checker = program.getTypeChecker();
29+
const seen = new Map<string, true>();
30+
return createCombinedCodeActions(textChanges.ChangeTracker.with(context, changes => {
31+
eachDiagnostic(context, errorCodes, diag => {
32+
const info = getInfo(diag.file, diag.start, checker);
33+
if (!info.length) {
34+
return;
35+
}
36+
for (const add of info) {
37+
if (addToSeen(seen, add.id + "")) {
38+
addUndefinedToOptionalProperty(changes, info);
39+
}
40+
}
41+
});
42+
}));
43+
},
44+
});
45+
46+
function getInfo(sourceFile: SourceFile, tokenPos: number, checker: TypeChecker): Symbol[] {
47+
// The target of the incorrect assignment
48+
// eg
49+
// this.definite = 1; -OR- definite = source
50+
// ^^^^ ^^^^^^^^
51+
const targetToken = getTokenAtPosition(sourceFile, tokenPos);
52+
const isOK = (isIdentifier(targetToken) || isPrivateIdentifier(targetToken))
53+
&& isBinaryExpression(targetToken.parent)
54+
&& targetToken.parent.operatorToken.kind === SyntaxKind.EqualsToken;
55+
if (!isOK) {
56+
// TODO: Walk up through lhs instead
57+
return [];
58+
}
59+
const sourceNode = targetToken.parent.right;
60+
// TODO: Also can apply to function calls, and then you have to get the signature, then its parameters, then the type of a particular parameter
61+
// TODO: Also skip 'any' and node_modules and if target is not in node_modules or is built-in
62+
return checker.getExactOptionalUnassignableProperties(checker.getTypeAtLocation(sourceNode), checker.getTypeAtLocation(targetToken))
63+
}
64+
65+
function addUndefinedToOptionalProperty(changes: textChanges.ChangeTracker, toAdd: Symbol[]) {
66+
for (const add of toAdd) {
67+
const d = add.valueDeclaration
68+
if (d && (isPropertySignature(d) || isPropertyDeclaration(d)) && d.type) {
69+
const t = factory.createUnionTypeNode([
70+
...d.type.kind === SyntaxKind.UnionType ? (d.type as UnionTypeNode).types : [d.type],
71+
factory.createTypeReferenceNode("undefined")
72+
])
73+
changes.replaceNode(d.getSourceFile(), d.type, t)
74+
}
75+
}
76+
}
77+
}

src/services/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
"codefixes/addMissingDeclareProperty.ts",
5858
"codefixes/addMissingInvocationForDecorator.ts",
5959
"codefixes/addNameToNamelessParameter.ts",
60+
"codefixes/addOptionalPropertyUndefined.ts",
6061
"codefixes/annotateWithTypeFromJSDoc.ts",
6162
"codefixes/convertFunctionToEs6Class.ts",
6263
"codefixes/convertToAsyncFunction.ts",
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/// <reference path='fourslash.ts'/>
2+
3+
// @strictNullChecks: true
4+
// @exactOptionalPropertyTypes: true
5+
////interface I {
6+
//// a?: number
7+
////}
8+
////interface J {
9+
//// a?: number | undefined
10+
////}
11+
////declare var i: I
12+
////declare var j: J
13+
////i/**/ = j
14+
verify.codeFixAvailable([
15+
{ description: ts.Diagnostics.Add_undefined_to_optional_property_type.message }
16+
]);
17+
18+
verify.codeFix({
19+
description: ts.Diagnostics.Add_undefined_to_optional_property_type.message,
20+
index: 0,
21+
newFileContent:
22+
`interface I {
23+
a?: number | undefined
24+
}
25+
interface J {
26+
a?: number | undefined
27+
}
28+
declare var i: I
29+
declare var j: J
30+
i = j`,
31+
});
32+

0 commit comments

Comments
 (0)