Skip to content

Commit 19260d1

Browse files
committed
Use intra-expression inference sites in type argument inference
1 parent b1a25fd commit 19260d1

File tree

2 files changed

+94
-31
lines changed

2 files changed

+94
-31
lines changed

src/compiler/checker.ts

Lines changed: 87 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -21729,6 +21729,9 @@ namespace ts {
2172921729
const inference = inferences[i];
2173021730
if (t === inference.typeParameter) {
2173121731
if (fix && !inference.isFixed) {
21732+
// Before we commit to a particular inference (and thus lock out any further inferences),
21733+
// we infer from any intra-expression inference sites we have collected.
21734+
inferFromIntraExpressionSites(context);
2173221735
clearCachedInferences(inferences);
2173321736
inference.isFixed = true;
2173421737
}
@@ -21746,6 +21749,37 @@ namespace ts {
2174621749
}
2174721750
}
2174821751

21752+
function addIntraExpressionInferenceSite(context: InferenceContext, node: Expression | MethodDeclaration, type: Type) {
21753+
(context.intraExpressionInferenceSites ??= []).push({ node, type });
21754+
}
21755+
21756+
// We collect intra-expression inference sites within object and array literals to handle cases where
21757+
// inferred types flow between context sensitive element expressions. For example:
21758+
//
21759+
// declare function foo<T>(arg: [(n: number) => T, (x: T) => void]): void;
21760+
// foo([_a => 0, n => n.toFixed()]);
21761+
//
21762+
// Above, both arrow functions in the tuple argument are context sensitive, thus both are omitted from the
21763+
// pass that collects inferences from the non-context sensitive parts of the arguments. In the subsequent
21764+
// pass where nothing is omitted, we need to commit to an inference for T in order to contextually type the
21765+
// parameter in the second arrow function, but we want to first infer from the return type of the first
21766+
// arrow function. This happens automatically when the arrow functions are discrete arguments (because we
21767+
// infer from each argument before processing the next), but when the arrow functions are elements of an
21768+
// object or array literal, we need to perform intra-expression inferences early.
21769+
function inferFromIntraExpressionSites(context: InferenceContext) {
21770+
if (context.intraExpressionInferenceSites) {
21771+
for (const { node, type } of context.intraExpressionInferenceSites) {
21772+
const contextualType = node.kind === SyntaxKind.MethodDeclaration ?
21773+
getContextualTypeForObjectLiteralMethod(node as MethodDeclaration, ContextFlags.NoConstraints) :
21774+
getContextualType(node, ContextFlags.NoConstraints);
21775+
if (contextualType) {
21776+
inferTypes(context.inferences, type, contextualType);
21777+
}
21778+
}
21779+
context.intraExpressionInferenceSites = undefined;
21780+
}
21781+
}
21782+
2174921783
function createInferenceInfo(typeParameter: TypeParameter): InferenceInfo {
2175021784
return {
2175121785
typeParameter,
@@ -27408,6 +27442,11 @@ namespace ts {
2740827442
const type = checkExpressionForMutableLocation(e, checkMode, elementContextualType, forceTuple);
2740927443
elementTypes.push(addOptionality(type, /*isProperty*/ true, hasOmittedExpression));
2741027444
elementFlags.push(hasOmittedExpression ? ElementFlags.Optional : ElementFlags.Required);
27445+
if (contextualType && someType(contextualType, isTupleLikeType) && checkMode && checkMode & CheckMode.Inferential && !(checkMode & CheckMode.SkipContextSensitive) && isContextSensitive(e)) {
27446+
const inferenceContext = getInferenceContext(node);
27447+
Debug.assert(inferenceContext); // In CheckMode.Inferential we should always have an inference context
27448+
addIntraExpressionInferenceSite(inferenceContext, e, type);
27449+
}
2741127450
}
2741227451
}
2741327452
if (inDestructuringPattern) {
@@ -27625,6 +27664,14 @@ namespace ts {
2762527664
prop.target = member;
2762627665
member = prop;
2762727666
allPropertiesTable?.set(prop.escapedName, prop);
27667+
27668+
if (contextualType && checkMode && checkMode & CheckMode.Inferential && !(checkMode & CheckMode.SkipContextSensitive) &&
27669+
(memberDecl.kind === SyntaxKind.PropertyAssignment || memberDecl.kind === SyntaxKind.MethodDeclaration) && isContextSensitive(memberDecl)) {
27670+
const inferenceContext = getInferenceContext(node);
27671+
Debug.assert(inferenceContext); // In CheckMode.Inferential we should always have an inference context
27672+
const inferenceNode = memberDecl.kind === SyntaxKind.PropertyAssignment ? memberDecl.initializer : memberDecl;
27673+
addIntraExpressionInferenceSite(inferenceContext, inferenceNode, type);
27674+
}
2762827675
}
2762927676
else if (memberDecl.kind === SyntaxKind.SpreadAssignment) {
2763027677
if (languageVersion < ScriptTarget.ES2015) {
@@ -29727,34 +29774,36 @@ namespace ts {
2972729774
if (node.kind !== SyntaxKind.Decorator) {
2972829775
const contextualType = getContextualType(node, every(signature.typeParameters, p => !!getDefaultFromTypeParameter(p)) ? ContextFlags.SkipBindingPatterns : ContextFlags.None);
2972929776
if (contextualType) {
29730-
// We clone the inference context to avoid disturbing a resolution in progress for an
29731-
// outer call expression. Effectively we just want a snapshot of whatever has been
29732-
// inferred for any outer call expression so far.
29733-
const outerContext = getInferenceContext(node);
29734-
const outerMapper = getMapperFromContext(cloneInferenceContext(outerContext, InferenceFlags.NoDefault));
29735-
const instantiatedType = instantiateType(contextualType, outerMapper);
29736-
// If the contextual type is a generic function type with a single call signature, we
29737-
// instantiate the type with its own type parameters and type arguments. This ensures that
29738-
// the type parameters are not erased to type any during type inference such that they can
29739-
// be inferred as actual types from the contextual type. For example:
29740-
// declare function arrayMap<T, U>(f: (x: T) => U): (a: T[]) => U[];
29741-
// const boxElements: <A>(a: A[]) => { value: A }[] = arrayMap(value => ({ value }));
29742-
// Above, the type of the 'value' parameter is inferred to be 'A'.
29743-
const contextualSignature = getSingleCallSignature(instantiatedType);
29744-
const inferenceSourceType = contextualSignature && contextualSignature.typeParameters ?
29745-
getOrCreateTypeFromSignature(getSignatureInstantiationWithoutFillingInTypeArguments(contextualSignature, contextualSignature.typeParameters)) :
29746-
instantiatedType;
2974729777
const inferenceTargetType = getReturnTypeOfSignature(signature);
29748-
// Inferences made from return types have lower priority than all other inferences.
29749-
inferTypes(context.inferences, inferenceSourceType, inferenceTargetType, InferencePriority.ReturnType);
29750-
// Create a type mapper for instantiating generic contextual types using the inferences made
29751-
// from the return type. We need a separate inference pass here because (a) instantiation of
29752-
// the source type uses the outer context's return mapper (which excludes inferences made from
29753-
// outer arguments), and (b) we don't want any further inferences going into this context.
29754-
const returnContext = createInferenceContext(signature.typeParameters!, signature, context.flags);
29755-
const returnSourceType = instantiateType(contextualType, outerContext && outerContext.returnMapper);
29756-
inferTypes(returnContext.inferences, returnSourceType, inferenceTargetType);
29757-
context.returnMapper = some(returnContext.inferences, hasInferenceCandidates) ? getMapperFromContext(cloneInferredPartOfContext(returnContext)) : undefined;
29778+
if (couldContainTypeVariables(inferenceTargetType)) {
29779+
// We clone the inference context to avoid disturbing a resolution in progress for an
29780+
// outer call expression. Effectively we just want a snapshot of whatever has been
29781+
// inferred for any outer call expression so far.
29782+
const outerContext = getInferenceContext(node);
29783+
const outerMapper = getMapperFromContext(cloneInferenceContext(outerContext, InferenceFlags.NoDefault));
29784+
const instantiatedType = instantiateType(contextualType, outerMapper);
29785+
// If the contextual type is a generic function type with a single call signature, we
29786+
// instantiate the type with its own type parameters and type arguments. This ensures that
29787+
// the type parameters are not erased to type any during type inference such that they can
29788+
// be inferred as actual types from the contextual type. For example:
29789+
// declare function arrayMap<T, U>(f: (x: T) => U): (a: T[]) => U[];
29790+
// const boxElements: <A>(a: A[]) => { value: A }[] = arrayMap(value => ({ value }));
29791+
// Above, the type of the 'value' parameter is inferred to be 'A'.
29792+
const contextualSignature = getSingleCallSignature(instantiatedType);
29793+
const inferenceSourceType = contextualSignature && contextualSignature.typeParameters ?
29794+
getOrCreateTypeFromSignature(getSignatureInstantiationWithoutFillingInTypeArguments(contextualSignature, contextualSignature.typeParameters)) :
29795+
instantiatedType;
29796+
// Inferences made from return types have lower priority than all other inferences.
29797+
inferTypes(context.inferences, inferenceSourceType, inferenceTargetType, InferencePriority.ReturnType);
29798+
// Create a type mapper for instantiating generic contextual types using the inferences made
29799+
// from the return type. We need a separate inference pass here because (a) instantiation of
29800+
// the source type uses the outer context's return mapper (which excludes inferences made from
29801+
// outer arguments), and (b) we don't want any further inferences going into this context.
29802+
const returnContext = createInferenceContext(signature.typeParameters!, signature, context.flags);
29803+
const returnSourceType = instantiateType(contextualType, outerContext && outerContext.returnMapper);
29804+
inferTypes(returnContext.inferences, returnSourceType, inferenceTargetType);
29805+
context.returnMapper = some(returnContext.inferences, hasInferenceCandidates) ? getMapperFromContext(cloneInferredPartOfContext(returnContext)) : undefined;
29806+
}
2975829807
}
2975929808
}
2976029809

@@ -29768,7 +29817,7 @@ namespace ts {
2976829817
}
2976929818

2977029819
const thisType = getThisTypeOfSignature(signature);
29771-
if (thisType) {
29820+
if (thisType && couldContainTypeVariables(thisType)) {
2977229821
const thisArgumentNode = getThisArgumentOfCall(node);
2977329822
inferTypes(context.inferences, getThisArgumentType(thisArgumentNode), thisType);
2977429823
}
@@ -29777,12 +29826,14 @@ namespace ts {
2977729826
const arg = args[i];
2977829827
if (arg.kind !== SyntaxKind.OmittedExpression && !(checkMode & CheckMode.IsForStringLiteralArgumentCompletions && hasSkipDirectInferenceFlag(arg))) {
2977929828
const paramType = getTypeAtPosition(signature, i);
29780-
const argType = checkExpressionWithContextualType(arg, paramType, context, checkMode);
29781-
inferTypes(context.inferences, argType, paramType);
29829+
if (couldContainTypeVariables(paramType)) {
29830+
const argType = checkExpressionWithContextualType(arg, paramType, context, checkMode);
29831+
inferTypes(context.inferences, argType, paramType);
29832+
}
2978229833
}
2978329834
}
2978429835

29785-
if (restType) {
29836+
if (restType && couldContainTypeVariables(restType)) {
2978629837
const spreadType = getSpreadArgumentType(args, argCount, args.length, restType, context, checkMode);
2978729838
inferTypes(context.inferences, spreadType, restType);
2978829839
}
@@ -34141,6 +34192,11 @@ namespace ts {
3414134192
context.contextualType = contextualType;
3414234193
context.inferenceContext = inferenceContext;
3414334194
const type = checkExpression(node, checkMode | CheckMode.Contextual | (inferenceContext ? CheckMode.Inferential : 0));
34195+
// In CheckMode.Inferential we collect intra-expression inference sites to process before fixing any type
34196+
// parameters. This information is no longer needed after the call to checkExpression.
34197+
if (inferenceContext && inferenceContext.intraExpressionInferenceSites) {
34198+
inferenceContext.intraExpressionInferenceSites = undefined;
34199+
}
3414434200
// We strip literal freshness when an appropriate contextual type is present such that contextually typed
3414534201
// literals always preserve their literal types (otherwise they might widen during type inference). An alternative
3414634202
// here would be to not mark contextually typed literals as fresh in the first place.

src/compiler/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5913,6 +5913,13 @@ namespace ts {
59135913
nonFixingMapper: TypeMapper; // Mapper that doesn't fix inferences
59145914
returnMapper?: TypeMapper; // Type mapper for inferences from return types (if any)
59155915
inferredTypeParameters?: readonly TypeParameter[]; // Inferred type parameters for function result
5916+
intraExpressionInferenceSites?: IntraExpressionInferenceSite[];
5917+
}
5918+
5919+
/* @internal */
5920+
export interface IntraExpressionInferenceSite {
5921+
node: Expression | MethodDeclaration;
5922+
type: Type;
59165923
}
59175924

59185925
/* @internal */

0 commit comments

Comments
 (0)