Skip to content

Commit 607f2ea

Browse files
authored
Merge pull request #29478 from Microsoft/fixContextualReturnTypes
Infer contextual types from generic return types
2 parents 9d3707d + 42be36d commit 607f2ea

15 files changed

+1899
-30
lines changed

src/compiler/checker.ts

+52-17
Original file line numberDiff line numberDiff line change
@@ -14636,18 +14636,18 @@ namespace ts {
1463614636
}
1463714637

1463814638
function inferFromProperties(source: Type, target: Type) {
14639-
if (isTupleType(source)) {
14639+
if (isArrayType(source) || isTupleType(source)) {
1464014640
if (isTupleType(target)) {
14641-
const sourceLength = getLengthOfTupleType(source);
14641+
const sourceLength = isTupleType(source) ? getLengthOfTupleType(source) : 0;
1464214642
const targetLength = getLengthOfTupleType(target);
14643-
const sourceRestType = getRestTypeOfTupleType(source);
14643+
const sourceRestType = isTupleType(source) ? getRestTypeOfTupleType(source) : getElementTypeOfArrayType(source);
1464414644
const targetRestType = getRestTypeOfTupleType(target);
1464514645
const fixedLength = targetLength < sourceLength || sourceRestType ? targetLength : sourceLength;
1464614646
for (let i = 0; i < fixedLength; i++) {
14647-
inferFromTypes(i < sourceLength ? source.typeArguments![i] : sourceRestType!, target.typeArguments![i]);
14647+
inferFromTypes(i < sourceLength ? (<TypeReference>source).typeArguments![i] : sourceRestType!, target.typeArguments![i]);
1464814648
}
1464914649
if (targetRestType) {
14650-
const types = fixedLength < sourceLength ? source.typeArguments!.slice(fixedLength, sourceLength) : [];
14650+
const types = fixedLength < sourceLength ? (<TypeReference>source).typeArguments!.slice(fixedLength, sourceLength) : [];
1465114651
if (sourceRestType) {
1465214652
types.push(sourceRestType);
1465314653
}
@@ -17759,19 +17759,49 @@ namespace ts {
1775917759
// Return the contextual type for a given expression node. During overload resolution, a contextual type may temporarily
1776017760
// be "pushed" onto a node using the contextualType property.
1776117761
function getApparentTypeOfContextualType(node: Expression): Type | undefined {
17762-
let contextualType = getContextualType(node);
17763-
contextualType = contextualType && mapType(contextualType, getApparentType);
17764-
if (contextualType && contextualType.flags & TypeFlags.Union) {
17765-
if (isObjectLiteralExpression(node)) {
17766-
return discriminateContextualTypeByObjectMembers(node, contextualType as UnionType);
17762+
const contextualType = instantiateContextualType(getContextualType(node), node);
17763+
if (contextualType) {
17764+
const apparentType = mapType(contextualType, getApparentType, /*noReductions*/ true);
17765+
if (apparentType.flags & TypeFlags.Union) {
17766+
if (isObjectLiteralExpression(node)) {
17767+
return discriminateContextualTypeByObjectMembers(node, apparentType as UnionType);
17768+
}
17769+
else if (isJsxAttributes(node)) {
17770+
return discriminateContextualTypeByJSXAttributes(node, apparentType as UnionType);
17771+
}
1776717772
}
17768-
else if (isJsxAttributes(node)) {
17769-
return discriminateContextualTypeByJSXAttributes(node, contextualType as UnionType);
17773+
return apparentType;
17774+
}
17775+
}
17776+
17777+
// If the given contextual type contains instantiable types and if a mapper representing
17778+
// return type inferences is available, instantiate those types using that mapper.
17779+
function instantiateContextualType(contextualType: Type | undefined, node: Expression): Type | undefined {
17780+
if (contextualType && maybeTypeOfKind(contextualType, TypeFlags.Instantiable)) {
17781+
const returnMapper = (<InferenceContext>getContextualMapper(node)).returnMapper;
17782+
if (returnMapper) {
17783+
return instantiateInstantiableTypes(contextualType, returnMapper);
1777017784
}
1777117785
}
1777217786
return contextualType;
1777317787
}
1777417788

17789+
// This function is similar to instantiateType, except that (a) it only instantiates types that
17790+
// are classified as instantiable (i.e. it doesn't instantiate object types), and (b) it performs
17791+
// no reductions on instantiated union types.
17792+
function instantiateInstantiableTypes(type: Type, mapper: TypeMapper): Type {
17793+
if (type.flags & TypeFlags.Instantiable) {
17794+
return instantiateType(type, mapper);
17795+
}
17796+
if (type.flags & TypeFlags.Union) {
17797+
return getUnionType(map((<UnionType>type).types, t => instantiateInstantiableTypes(t, mapper)), UnionReduction.None);
17798+
}
17799+
if (type.flags & TypeFlags.Intersection) {
17800+
return getIntersectionType(map((<IntersectionType>type).types, t => instantiateInstantiableTypes(t, mapper)));
17801+
}
17802+
return type;
17803+
}
17804+
1777517805
/**
1777617806
* Woah! Do you really want to use this function?
1777717807
*
@@ -19910,6 +19940,9 @@ namespace ts {
1991019940
const inferenceTargetType = getReturnTypeOfSignature(signature);
1991119941
// Inferences made from return types have lower priority than all other inferences.
1991219942
inferTypes(context.inferences, inferenceSourceType, inferenceTargetType, InferencePriority.ReturnType);
19943+
// Create a type mapper for instantiating generic contextual types using the inferences made
19944+
// from the return type.
19945+
context.returnMapper = cloneTypeMapper(context);
1991319946
}
1991419947
}
1991519948

@@ -23020,7 +23053,12 @@ namespace ts {
2302023053
context.contextualMapper = contextualMapper;
2302123054
const checkMode = contextualMapper === identityMapper ? CheckMode.SkipContextSensitive :
2302223055
contextualMapper ? CheckMode.Inferential : CheckMode.Contextual;
23023-
const result = checkExpression(node, checkMode);
23056+
const type = checkExpression(node, checkMode);
23057+
// We strip literal freshness when an appropriate contextual type is present such that contextually typed
23058+
// literals always preserve their literal types (otherwise they might widen during type inference). An alternative
23059+
// here would be to not mark contextually typed literals as fresh in the first place.
23060+
const result = maybeTypeOfKind(type, TypeFlags.Literal) && isLiteralOfContextualType(type, instantiateContextualType(contextualType, node)) ?
23061+
getRegularTypeOfLiteralType(type) : type;
2302423062
context.contextualType = saveContextualType;
2302523063
context.contextualMapper = saveContextualMapper;
2302623064
return result;
@@ -23104,13 +23142,10 @@ namespace ts {
2310423142
}
2310523143

2310623144
function checkExpressionForMutableLocation(node: Expression, checkMode: CheckMode | undefined, contextualType?: Type, forceTuple?: boolean): Type {
23107-
if (arguments.length === 2) {
23108-
contextualType = getContextualType(node);
23109-
}
2311023145
const type = checkExpression(node, checkMode, forceTuple);
2311123146
return isConstContext(node) ? getRegularTypeOfLiteralType(type) :
2311223147
isTypeAssertion(node) ? type :
23113-
getWidenedLiteralLikeTypeForContextualType(type, contextualType);
23148+
getWidenedLiteralLikeTypeForContextualType(type, instantiateContextualType(arguments.length === 2 ? getContextualType(node) : contextualType, node));
2311423149
}
2311523150

2311623151
function checkPropertyAssignment(node: PropertyAssignment, checkMode?: CheckMode): Type {

src/compiler/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -4370,6 +4370,7 @@ namespace ts {
43704370
inferences: InferenceInfo[]; // Inferences made for each type parameter
43714371
flags: InferenceFlags; // Inference flags
43724372
compareTypes: TypeComparer; // Type comparer function
4373+
returnMapper?: TypeMapper; // Type mapper for inferences from return types (if any)
43734374
}
43744375

43754376
/* @internal */

tests/baselines/reference/contextualTypeShouldBeLiteral.js

+38-1
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,34 @@ let xyz: LikeA | LikeB = {
9393
}
9494
};
9595

96-
xyz;
96+
xyz;
97+
98+
// Repro from #29168
99+
100+
interface TestObject {
101+
type?: 'object';
102+
items: {
103+
[k: string]: TestGeneric;
104+
};
105+
}
106+
107+
interface TestString {
108+
type: 'string';
109+
}
110+
111+
type TestGeneric = (TestString | TestObject) & { [k: string]: any; };
112+
113+
const test: TestGeneric = {
114+
items: {
115+
hello: { type: 'string' },
116+
world: {
117+
items: {
118+
nested: { type: 'string' }
119+
}
120+
}
121+
}
122+
};
123+
97124

98125
//// [contextualTypeShouldBeLiteral.js]
99126
"use strict";
@@ -134,3 +161,13 @@ var xyz = {
134161
}
135162
};
136163
xyz;
164+
var test = {
165+
items: {
166+
hello: { type: 'string' },
167+
world: {
168+
items: {
169+
nested: { type: 'string' }
170+
}
171+
}
172+
}
173+
};

tests/baselines/reference/contextualTypeShouldBeLiteral.symbols

+56
Original file line numberDiff line numberDiff line change
@@ -227,3 +227,59 @@ let xyz: LikeA | LikeB = {
227227
xyz;
228228
>xyz : Symbol(xyz, Decl(contextualTypeShouldBeLiteral.ts, 82, 3))
229229

230+
// Repro from #29168
231+
232+
interface TestObject {
233+
>TestObject : Symbol(TestObject, Decl(contextualTypeShouldBeLiteral.ts, 94, 4))
234+
235+
type?: 'object';
236+
>type : Symbol(TestObject.type, Decl(contextualTypeShouldBeLiteral.ts, 98, 22))
237+
238+
items: {
239+
>items : Symbol(TestObject.items, Decl(contextualTypeShouldBeLiteral.ts, 99, 18))
240+
241+
[k: string]: TestGeneric;
242+
>k : Symbol(k, Decl(contextualTypeShouldBeLiteral.ts, 101, 5))
243+
>TestGeneric : Symbol(TestGeneric, Decl(contextualTypeShouldBeLiteral.ts, 107, 1))
244+
245+
};
246+
}
247+
248+
interface TestString {
249+
>TestString : Symbol(TestString, Decl(contextualTypeShouldBeLiteral.ts, 103, 1))
250+
251+
type: 'string';
252+
>type : Symbol(TestString.type, Decl(contextualTypeShouldBeLiteral.ts, 105, 22))
253+
}
254+
255+
type TestGeneric = (TestString | TestObject) & { [k: string]: any; };
256+
>TestGeneric : Symbol(TestGeneric, Decl(contextualTypeShouldBeLiteral.ts, 107, 1))
257+
>TestString : Symbol(TestString, Decl(contextualTypeShouldBeLiteral.ts, 103, 1))
258+
>TestObject : Symbol(TestObject, Decl(contextualTypeShouldBeLiteral.ts, 94, 4))
259+
>k : Symbol(k, Decl(contextualTypeShouldBeLiteral.ts, 109, 50))
260+
261+
const test: TestGeneric = {
262+
>test : Symbol(test, Decl(contextualTypeShouldBeLiteral.ts, 111, 5))
263+
>TestGeneric : Symbol(TestGeneric, Decl(contextualTypeShouldBeLiteral.ts, 107, 1))
264+
265+
items: {
266+
>items : Symbol(items, Decl(contextualTypeShouldBeLiteral.ts, 111, 27))
267+
268+
hello: { type: 'string' },
269+
>hello : Symbol(hello, Decl(contextualTypeShouldBeLiteral.ts, 112, 10))
270+
>type : Symbol(type, Decl(contextualTypeShouldBeLiteral.ts, 113, 12))
271+
272+
world: {
273+
>world : Symbol(world, Decl(contextualTypeShouldBeLiteral.ts, 113, 30))
274+
275+
items: {
276+
>items : Symbol(items, Decl(contextualTypeShouldBeLiteral.ts, 114, 12))
277+
278+
nested: { type: 'string' }
279+
>nested : Symbol(nested, Decl(contextualTypeShouldBeLiteral.ts, 115, 14))
280+
>type : Symbol(type, Decl(contextualTypeShouldBeLiteral.ts, 116, 17))
281+
}
282+
}
283+
}
284+
};
285+

tests/baselines/reference/contextualTypeShouldBeLiteral.types

+56
Original file line numberDiff line numberDiff line change
@@ -222,3 +222,59 @@ let xyz: LikeA | LikeB = {
222222
xyz;
223223
>xyz : LikeA
224224

225+
// Repro from #29168
226+
227+
interface TestObject {
228+
type?: 'object';
229+
>type : "object" | undefined
230+
231+
items: {
232+
>items : { [k: string]: TestGeneric; }
233+
234+
[k: string]: TestGeneric;
235+
>k : string
236+
237+
};
238+
}
239+
240+
interface TestString {
241+
type: 'string';
242+
>type : "string"
243+
}
244+
245+
type TestGeneric = (TestString | TestObject) & { [k: string]: any; };
246+
>TestGeneric : TestGeneric
247+
>k : string
248+
249+
const test: TestGeneric = {
250+
>test : TestGeneric
251+
>{ items: { hello: { type: 'string' }, world: { items: { nested: { type: 'string' } } } }} : { items: { hello: { type: "string"; }; world: { items: { nested: { type: "string"; }; }; }; }; }
252+
253+
items: {
254+
>items : { hello: { type: "string"; }; world: { items: { nested: { type: "string"; }; }; }; }
255+
>{ hello: { type: 'string' }, world: { items: { nested: { type: 'string' } } } } : { hello: { type: "string"; }; world: { items: { nested: { type: "string"; }; }; }; }
256+
257+
hello: { type: 'string' },
258+
>hello : { type: "string"; }
259+
>{ type: 'string' } : { type: "string"; }
260+
>type : "string"
261+
>'string' : "string"
262+
263+
world: {
264+
>world : { items: { nested: { type: "string"; }; }; }
265+
>{ items: { nested: { type: 'string' } } } : { items: { nested: { type: "string"; }; }; }
266+
267+
items: {
268+
>items : { nested: { type: "string"; }; }
269+
>{ nested: { type: 'string' } } : { nested: { type: "string"; }; }
270+
271+
nested: { type: 'string' }
272+
>nested : { type: "string"; }
273+
>{ type: 'string' } : { type: "string"; }
274+
>type : "string"
275+
>'string' : "string"
276+
}
277+
}
278+
}
279+
};
280+

tests/baselines/reference/errorMessagesIntersectionTypes02.errors.txt

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
tests/cases/compiler/errorMessagesIntersectionTypes02.ts(14,5): error TS2322: Type '{ fooProp: string; } & Bar' is not assignable to type 'FooBar'.
1+
tests/cases/compiler/errorMessagesIntersectionTypes02.ts(14,5): error TS2322: Type '{ fooProp: "frizzlebizzle"; } & Bar' is not assignable to type 'FooBar'.
22
Types of property 'fooProp' are incompatible.
3-
Type 'string' is not assignable to type '"hello" | "world"'.
3+
Type '"frizzlebizzle"' is not assignable to type '"hello" | "world"'.
44

55

66
==== tests/cases/compiler/errorMessagesIntersectionTypes02.ts (1 errors) ====
@@ -19,8 +19,8 @@ tests/cases/compiler/errorMessagesIntersectionTypes02.ts(14,5): error TS2322: Ty
1919

2020
let fooBar: FooBar = mixBar({
2121
~~~~~~
22-
!!! error TS2322: Type '{ fooProp: string; } & Bar' is not assignable to type 'FooBar'.
22+
!!! error TS2322: Type '{ fooProp: "frizzlebizzle"; } & Bar' is not assignable to type 'FooBar'.
2323
!!! error TS2322: Types of property 'fooProp' are incompatible.
24-
!!! error TS2322: Type 'string' is not assignable to type '"hello" | "world"'.
24+
!!! error TS2322: Type '"frizzlebizzle"' is not assignable to type '"hello" | "world"'.
2525
fooProp: "frizzlebizzle"
2626
});

tests/baselines/reference/errorMessagesIntersectionTypes02.types

+3-3
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@ declare function mixBar<T>(obj: T): T & Bar;
1818

1919
let fooBar: FooBar = mixBar({
2020
>fooBar : FooBar
21-
>mixBar({ fooProp: "frizzlebizzle"}) : { fooProp: string; } & Bar
21+
>mixBar({ fooProp: "frizzlebizzle"}) : { fooProp: "frizzlebizzle"; } & Bar
2222
>mixBar : <T>(obj: T) => T & Bar
23-
>{ fooProp: "frizzlebizzle"} : { fooProp: string; }
23+
>{ fooProp: "frizzlebizzle"} : { fooProp: "frizzlebizzle"; }
2424

2525
fooProp: "frizzlebizzle"
26-
>fooProp : string
26+
>fooProp : "frizzlebizzle"
2727
>"frizzlebizzle" : "frizzlebizzle"
2828

2929
});

0 commit comments

Comments
 (0)