Skip to content

Commit f1370ec

Browse files
authored
Allow special assignments to have a contextual type of their declared type if present (#26802)
* Allow special assignments to have a contextual type of their declared type if present * Expand change to cover all js special assignments * Remove extraneous line
1 parent 262ea5b commit f1370ec

10 files changed

+393
-18
lines changed

src/compiler/binder.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2080,8 +2080,8 @@ namespace ts {
20802080
if (isInJavaScriptFile(node) &&
20812081
file.commonJsModuleIndicator &&
20822082
isModuleExportsPropertyAccessExpression(node as PropertyAccessExpression) &&
2083-
!lookupSymbolForNameWorker(container, "module" as __String)) {
2084-
declareSymbol(container.locals!, /*parent*/ undefined, (node as PropertyAccessExpression).expression as Identifier,
2083+
!lookupSymbolForNameWorker(blockScopeContainer, "module" as __String)) {
2084+
declareSymbol(file.locals!, /*parent*/ undefined, (node as PropertyAccessExpression).expression as Identifier,
20852085
SymbolFlags.FunctionScopedVariable | SymbolFlags.ModuleExports, SymbolFlags.FunctionScopedVariableExcludes);
20862086
}
20872087
break;

src/compiler/checker.ts

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16122,7 +16122,14 @@ namespace ts {
1612216122
const { left, operatorToken, right } = binaryExpression;
1612316123
switch (operatorToken.kind) {
1612416124
case SyntaxKind.EqualsToken:
16125-
return node === right && isContextSensitiveAssignment(binaryExpression) ? getTypeOfExpression(left) : undefined;
16125+
if (node !== right) {
16126+
return undefined;
16127+
}
16128+
const contextSensitive = getIsContextSensitiveAssignmentOrContextType(binaryExpression);
16129+
if (!contextSensitive) {
16130+
return undefined;
16131+
}
16132+
return contextSensitive === true ? getTypeOfExpression(left) : contextSensitive;
1612616133
case SyntaxKind.BarBarToken:
1612716134
// When an || expression has a contextual type, the operands are contextually typed by that type. When an ||
1612816135
// expression has no contextual type, the right operand is contextually typed by the type of the left operand,
@@ -16140,7 +16147,7 @@ namespace ts {
1614016147

1614116148
// In an assignment expression, the right operand is contextually typed by the type of the left operand.
1614216149
// Don't do this for special property assignments unless there is a type tag on the assignment, to avoid circularity from checking the right operand.
16143-
function isContextSensitiveAssignment(binaryExpression: BinaryExpression): boolean {
16150+
function getIsContextSensitiveAssignmentOrContextType(binaryExpression: BinaryExpression): boolean | Type {
1614416151
const kind = getSpecialPropertyAssignmentKind(binaryExpression);
1614516152
switch (kind) {
1614616153
case SpecialPropertyAssignmentKind.None:
@@ -16159,29 +16166,44 @@ namespace ts {
1615916166
if (!decl) {
1616016167
return false;
1616116168
}
16162-
if (isInJavaScriptFile(decl)) {
16163-
return !!getJSDocTypeTag(decl);
16169+
const lhs = binaryExpression.left as PropertyAccessExpression;
16170+
const overallAnnotation = getEffectiveTypeAnnotationNode(decl);
16171+
if (overallAnnotation) {
16172+
return getTypeFromTypeNode(overallAnnotation);
1616416173
}
16165-
else if (isIdentifier((binaryExpression.left as PropertyAccessExpression).expression)) {
16166-
const id = (binaryExpression.left as PropertyAccessExpression).expression as Identifier;
16174+
else if (isIdentifier(lhs.expression)) {
16175+
const id = lhs.expression;
1616716176
const parentSymbol = resolveName(id, id.escapedText, SymbolFlags.Value, undefined, id.escapedText, /*isUse*/ true);
16168-
return !isFunctionSymbol(parentSymbol);
16177+
if (parentSymbol && isFunctionSymbol(parentSymbol)) {
16178+
const annotated = getEffectiveTypeAnnotationNode(parentSymbol.valueDeclaration);
16179+
if (annotated) {
16180+
const type = getTypeOfPropertyOfContextualType(getTypeFromTypeNode(annotated), lhs.name.escapedText);
16181+
return type || false;
16182+
}
16183+
return false;
16184+
}
1616916185
}
16170-
return true;
16186+
return !isInJavaScriptFile(decl);
1617116187
}
16188+
case SpecialPropertyAssignmentKind.ModuleExports:
1617216189
case SpecialPropertyAssignmentKind.ThisProperty:
16173-
if (!binaryExpression.symbol ||
16174-
binaryExpression.symbol.valueDeclaration && !!getJSDocTypeTag(binaryExpression.symbol.valueDeclaration)) {
16175-
return true;
16190+
if (!binaryExpression.symbol) return true;
16191+
if (binaryExpression.symbol.valueDeclaration) {
16192+
const annotated = getEffectiveTypeAnnotationNode(binaryExpression.symbol.valueDeclaration);
16193+
if (annotated) {
16194+
const type = getTypeFromTypeNode(annotated);
16195+
if (type) {
16196+
return type;
16197+
}
16198+
}
1617616199
}
16200+
if (kind === SpecialPropertyAssignmentKind.ModuleExports) return false;
1617716201
const thisAccess = binaryExpression.left as PropertyAccessExpression;
1617816202
if (!isObjectLiteralMethod(getThisContainer(thisAccess.expression, /*includeArrowFunctions*/ false))) {
1617916203
return false;
1618016204
}
1618116205
const thisType = checkThisExpression(thisAccess.expression);
16182-
return thisType && !!getPropertyOfType(thisType, thisAccess.name.escapedText);
16183-
case SpecialPropertyAssignmentKind.ModuleExports:
16184-
return !binaryExpression.symbol || binaryExpression.symbol.valueDeclaration && !!getJSDocTypeTag(binaryExpression.symbol.valueDeclaration);
16206+
return thisType && getTypeOfPropertyOfContextualType(thisType, thisAccess.name.escapedText) || false;
1618516207
default:
1618616208
return Debug.assertNever(kind);
1618716209
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
//// [expandoFunctionContextualTypes.ts]
2+
interface MyComponentProps {
3+
color: "red" | "blue"
4+
}
5+
6+
interface StatelessComponent<P> {
7+
(): any;
8+
defaultProps?: Partial<P>;
9+
}
10+
11+
const MyComponent: StatelessComponent<MyComponentProps> = () => null as any;
12+
13+
MyComponent.defaultProps = {
14+
color: "red"
15+
};
16+
17+
18+
//// [expandoFunctionContextualTypes.js]
19+
var MyComponent = function () { return null; };
20+
MyComponent.defaultProps = {
21+
color: "red"
22+
};
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
=== tests/cases/compiler/expandoFunctionContextualTypes.ts ===
2+
interface MyComponentProps {
3+
>MyComponentProps : Symbol(MyComponentProps, Decl(expandoFunctionContextualTypes.ts, 0, 0))
4+
5+
color: "red" | "blue"
6+
>color : Symbol(MyComponentProps.color, Decl(expandoFunctionContextualTypes.ts, 0, 28))
7+
}
8+
9+
interface StatelessComponent<P> {
10+
>StatelessComponent : Symbol(StatelessComponent, Decl(expandoFunctionContextualTypes.ts, 2, 1))
11+
>P : Symbol(P, Decl(expandoFunctionContextualTypes.ts, 4, 29))
12+
13+
(): any;
14+
defaultProps?: Partial<P>;
15+
>defaultProps : Symbol(StatelessComponent.defaultProps, Decl(expandoFunctionContextualTypes.ts, 5, 12))
16+
>Partial : Symbol(Partial, Decl(lib.es5.d.ts, --, --))
17+
>P : Symbol(P, Decl(expandoFunctionContextualTypes.ts, 4, 29))
18+
}
19+
20+
const MyComponent: StatelessComponent<MyComponentProps> = () => null as any;
21+
>MyComponent : Symbol(MyComponent, Decl(expandoFunctionContextualTypes.ts, 9, 5))
22+
>StatelessComponent : Symbol(StatelessComponent, Decl(expandoFunctionContextualTypes.ts, 2, 1))
23+
>MyComponentProps : Symbol(MyComponentProps, Decl(expandoFunctionContextualTypes.ts, 0, 0))
24+
25+
MyComponent.defaultProps = {
26+
>MyComponent.defaultProps : Symbol(StatelessComponent.defaultProps, Decl(expandoFunctionContextualTypes.ts, 5, 12))
27+
>MyComponent : Symbol(MyComponent, Decl(expandoFunctionContextualTypes.ts, 9, 5))
28+
>defaultProps : Symbol(StatelessComponent.defaultProps, Decl(expandoFunctionContextualTypes.ts, 5, 12))
29+
30+
color: "red"
31+
>color : Symbol(color, Decl(expandoFunctionContextualTypes.ts, 11, 28))
32+
33+
};
34+
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
=== tests/cases/compiler/expandoFunctionContextualTypes.ts ===
2+
interface MyComponentProps {
3+
color: "red" | "blue"
4+
>color : "red" | "blue"
5+
}
6+
7+
interface StatelessComponent<P> {
8+
(): any;
9+
defaultProps?: Partial<P>;
10+
>defaultProps : Partial<P>
11+
}
12+
13+
const MyComponent: StatelessComponent<MyComponentProps> = () => null as any;
14+
>MyComponent : StatelessComponent<MyComponentProps>
15+
>() => null as any : { (): any; defaultProps: { color: "red"; }; }
16+
>null as any : any
17+
>null : null
18+
19+
MyComponent.defaultProps = {
20+
>MyComponent.defaultProps = { color: "red"} : { color: "red"; }
21+
>MyComponent.defaultProps : Partial<MyComponentProps>
22+
>MyComponent : StatelessComponent<MyComponentProps>
23+
>defaultProps : Partial<MyComponentProps>
24+
>{ color: "red"} : { color: "red"; }
25+
26+
color: "red"
27+
>color : "red"
28+
>"red" : "red"
29+
30+
};
31+
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
=== tests/cases/compiler/input.js ===
2+
/** @typedef {{ color: "red" | "blue" }} MyComponentProps */
3+
4+
/**
5+
* @template P
6+
* @typedef {{ (): any; defaultProps?: Partial<P> }} StatelessComponent */
7+
8+
/**
9+
* @type {StatelessComponent<MyComponentProps>}
10+
*/
11+
const MyComponent = () => /* @type {any} */(null);
12+
>MyComponent : Symbol(MyComponent, Decl(input.js, 9, 5))
13+
14+
MyComponent.defaultProps = {
15+
>MyComponent.defaultProps : Symbol(defaultProps, Decl(input.js, 4, 23))
16+
>MyComponent : Symbol(MyComponent, Decl(input.js, 9, 5))
17+
>defaultProps : Symbol(defaultProps, Decl(input.js, 4, 23))
18+
19+
color: "red"
20+
>color : Symbol(color, Decl(input.js, 11, 28))
21+
22+
};
23+
24+
const MyComponent2 = () => null;
25+
>MyComponent2 : Symbol(MyComponent2, Decl(input.js, 15, 5))
26+
27+
/**
28+
* @type {MyComponentProps}
29+
*/
30+
MyComponent2.defaultProps = {
31+
>MyComponent2.defaultProps : Symbol(MyComponent2.defaultProps, Decl(input.js, 15, 32))
32+
>MyComponent2 : Symbol(MyComponent2, Decl(input.js, 15, 5))
33+
>defaultProps : Symbol(MyComponent2.defaultProps, Decl(input.js, 15, 32))
34+
35+
color: "red"
36+
>color : Symbol(color, Decl(input.js, 20, 29))
37+
}
38+
39+
/**
40+
* @type {StatelessComponent<MyComponentProps>}
41+
*/
42+
const check = MyComponent2;
43+
>check : Symbol(check, Decl(input.js, 27, 5))
44+
>MyComponent2 : Symbol(MyComponent2, Decl(input.js, 15, 5))
45+
46+
/**
47+
*
48+
* @param {{ props: MyComponentProps }} p
49+
*/
50+
function expectLiteral(p) {}
51+
>expectLiteral : Symbol(expectLiteral, Decl(input.js, 27, 27))
52+
>p : Symbol(p, Decl(input.js, 33, 23))
53+
54+
function foo() {
55+
>foo : Symbol(foo, Decl(input.js, 33, 28))
56+
57+
/**
58+
* @type {MyComponentProps}
59+
*/
60+
this.props = { color: "red" };
61+
>props : Symbol(foo.props, Decl(input.js, 35, 16))
62+
>color : Symbol(color, Decl(input.js, 39, 18))
63+
64+
expectLiteral(this);
65+
>expectLiteral : Symbol(expectLiteral, Decl(input.js, 27, 27))
66+
}
67+
68+
/**
69+
* @type {MyComponentProps}
70+
*/
71+
module.exports = {
72+
>module.exports : Symbol("tests/cases/compiler/input", Decl(input.js, 0, 0))
73+
>module : Symbol(export=, Decl(input.js, 42, 1))
74+
>exports : Symbol(export=, Decl(input.js, 42, 1))
75+
76+
color: "red"
77+
>color : Symbol(color, Decl(input.js, 47, 18))
78+
}
79+
80+
expectLiteral({ props: module.exports });
81+
>expectLiteral : Symbol(expectLiteral, Decl(input.js, 27, 27))
82+
>props : Symbol(props, Decl(input.js, 51, 15))
83+
>module.exports : Symbol("tests/cases/compiler/input", Decl(input.js, 0, 0))
84+
>module : Symbol(module, Decl(input.js, 42, 1), Decl(input.js, 51, 22))
85+
>exports : Symbol("tests/cases/compiler/input", Decl(input.js, 0, 0))
86+
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
=== tests/cases/compiler/input.js ===
2+
/** @typedef {{ color: "red" | "blue" }} MyComponentProps */
3+
4+
/**
5+
* @template P
6+
* @typedef {{ (): any; defaultProps?: Partial<P> }} StatelessComponent */
7+
8+
/**
9+
* @type {StatelessComponent<MyComponentProps>}
10+
*/
11+
const MyComponent = () => /* @type {any} */(null);
12+
>MyComponent : { (): any; defaultProps?: Partial<{ color: "red" | "blue"; }>; }
13+
>() => /* @type {any} */(null) : { (): any; defaultProps: { color: "red"; }; }
14+
>(null) : null
15+
>null : null
16+
17+
MyComponent.defaultProps = {
18+
>MyComponent.defaultProps = { color: "red"} : { color: "red"; }
19+
>MyComponent.defaultProps : Partial<{ color: "red" | "blue"; }>
20+
>MyComponent : { (): any; defaultProps?: Partial<{ color: "red" | "blue"; }>; }
21+
>defaultProps : Partial<{ color: "red" | "blue"; }>
22+
>{ color: "red"} : { color: "red"; }
23+
24+
color: "red"
25+
>color : "red"
26+
>"red" : "red"
27+
28+
};
29+
30+
const MyComponent2 = () => null;
31+
>MyComponent2 : { (): any; defaultProps: { color: "red" | "blue"; }; }
32+
>() => null : { (): any; defaultProps: { color: "red" | "blue"; }; }
33+
>null : null
34+
35+
/**
36+
* @type {MyComponentProps}
37+
*/
38+
MyComponent2.defaultProps = {
39+
>MyComponent2.defaultProps = { color: "red"} : { color: "red"; }
40+
>MyComponent2.defaultProps : { color: "red" | "blue"; }
41+
>MyComponent2 : { (): any; defaultProps: { color: "red" | "blue"; }; }
42+
>defaultProps : { color: "red" | "blue"; }
43+
>{ color: "red"} : { color: "red"; }
44+
45+
color: "red"
46+
>color : "red"
47+
>"red" : "red"
48+
}
49+
50+
/**
51+
* @type {StatelessComponent<MyComponentProps>}
52+
*/
53+
const check = MyComponent2;
54+
>check : { (): any; defaultProps?: Partial<{ color: "red" | "blue"; }>; }
55+
>MyComponent2 : { (): any; defaultProps: { color: "red" | "blue"; }; }
56+
57+
/**
58+
*
59+
* @param {{ props: MyComponentProps }} p
60+
*/
61+
function expectLiteral(p) {}
62+
>expectLiteral : (p: { props: { color: "red" | "blue"; }; }) => void
63+
>p : { props: { color: "red" | "blue"; }; }
64+
65+
function foo() {
66+
>foo : typeof foo
67+
68+
/**
69+
* @type {MyComponentProps}
70+
*/
71+
this.props = { color: "red" };
72+
>this.props = { color: "red" } : { color: "red"; }
73+
>this.props : any
74+
>this : any
75+
>props : any
76+
>{ color: "red" } : { color: "red"; }
77+
>color : "red"
78+
>"red" : "red"
79+
80+
expectLiteral(this);
81+
>expectLiteral(this) : void
82+
>expectLiteral : (p: { props: { color: "red" | "blue"; }; }) => void
83+
>this : any
84+
}
85+
86+
/**
87+
* @type {MyComponentProps}
88+
*/
89+
module.exports = {
90+
>module.exports = { color: "red"} : { color: "red" | "blue"; }
91+
>module.exports : { color: "red" | "blue"; }
92+
>module : { "tests/cases/compiler/input": { color: "red" | "blue"; }; }
93+
>exports : { color: "red" | "blue"; }
94+
>{ color: "red"} : { color: "red"; }
95+
96+
color: "red"
97+
>color : "red"
98+
>"red" : "red"
99+
}
100+
101+
expectLiteral({ props: module.exports });
102+
>expectLiteral({ props: module.exports }) : void
103+
>expectLiteral : (p: { props: { color: "red" | "blue"; }; }) => void
104+
>{ props: module.exports } : { props: { color: "red" | "blue"; }; }
105+
>props : { color: "red" | "blue"; }
106+
>module.exports : { color: "red" | "blue"; }
107+
>module : { "tests/cases/compiler/input": { color: "red" | "blue"; }; }
108+
>exports : { color: "red" | "blue"; }
109+

tests/baselines/reference/moduleExportAssignment2.symbols

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ var npm = module.exports = function (tree) {
99
module.exports.asReadInstalled = function (tree) {
1010
>module.exports.asReadInstalled : Symbol(asReadInstalled, Decl(npm.js, 1, 1))
1111
>module.exports : Symbol(asReadInstalled, Decl(npm.js, 1, 1))
12-
>module : Symbol(module, Decl(npm.js, 0, 9))
12+
>module : Symbol(module, Decl(npm.js, 0, 9), Decl(npm.js, 3, 13))
1313
>exports : Symbol("tests/cases/conformance/salsa/npm", Decl(npm.js, 0, 0))
1414
>asReadInstalled : Symbol(asReadInstalled, Decl(npm.js, 1, 1))
1515
>tree : Symbol(tree, Decl(npm.js, 2, 43))
@@ -20,7 +20,7 @@ module.exports.asReadInstalled = function (tree) {
2020

2121
module.exports(tree)
2222
>module.exports : Symbol("tests/cases/conformance/salsa/npm", Decl(npm.js, 0, 0))
23-
>module : Symbol(module, Decl(npm.js, 3, 13))
23+
>module : Symbol(module, Decl(npm.js, 0, 9), Decl(npm.js, 3, 13))
2424
>exports : Symbol("tests/cases/conformance/salsa/npm", Decl(npm.js, 0, 0))
2525
>tree : Symbol(tree, Decl(npm.js, 2, 43))
2626
}

0 commit comments

Comments
 (0)