-
Notifications
You must be signed in to change notification settings - Fork 13k
Improve inference for context sensitive functions within reverse mapped types #54029
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
e90edb3
557cd99
4774f8f
b7eae05
a0804b5
57b54c9
fae5ae6
34d675a
a8e8a24
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -430,6 +430,7 @@ import { | |
InternalSymbolName, | ||
IntersectionType, | ||
IntersectionTypeNode, | ||
IntraExpressionInferenceSite, | ||
intrinsicTagNameToString, | ||
IntrinsicType, | ||
introducesArgumentsExoticObject, | ||
|
@@ -23898,7 +23899,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { | |
if (!inference.isFixed) { | ||
// Before we commit to a particular inference (and thus lock out any further inferences), | ||
// we infer from any intra-expression inference sites we have collected. | ||
inferFromIntraExpressionSites(context); | ||
if (context.intraExpressionInferenceSites) { | ||
inferFromIntraExpressionSites(context.inferences, context.intraExpressionInferenceSites); | ||
} | ||
clearCachedInferences(context.inferences); | ||
inference.isFixed = true; | ||
} | ||
|
@@ -23937,17 +23940,14 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { | |
// arrow function. This happens automatically when the arrow functions are discrete arguments (because we | ||
// infer from each argument before processing the next), but when the arrow functions are elements of an | ||
// object or array literal, we need to perform intra-expression inferences early. | ||
function inferFromIntraExpressionSites(context: InferenceContext) { | ||
if (context.intraExpressionInferenceSites) { | ||
for (const { node, type } of context.intraExpressionInferenceSites) { | ||
const contextualType = node.kind === SyntaxKind.MethodDeclaration ? | ||
getContextualTypeForObjectLiteralMethod(node as MethodDeclaration, ContextFlags.NoConstraints) : | ||
getContextualType(node, ContextFlags.NoConstraints); | ||
if (contextualType) { | ||
inferTypes(context.inferences, type, contextualType); | ||
} | ||
function inferFromIntraExpressionSites(inferences: InferenceInfo[], intraExpressionInferenceSites: IntraExpressionInferenceSite[]) { | ||
for (const { node, type } of intraExpressionInferenceSites) { | ||
const contextualType = node.kind === SyntaxKind.MethodDeclaration ? | ||
getContextualTypeForObjectLiteralMethod(node as MethodDeclaration, ContextFlags.NoConstraints) : | ||
getContextualType(node, ContextFlags.NoConstraints); | ||
if (contextualType) { | ||
inferTypes(inferences, type, contextualType); | ||
} | ||
context.intraExpressionInferenceSites = undefined; | ||
} | ||
} | ||
|
||
|
@@ -24071,20 +24071,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { | |
return type; | ||
} | ||
|
||
// We consider a type to be partially inferable if it isn't marked non-inferable or if it is | ||
// an object literal type with at least one property of an inferable type. For example, an object | ||
// literal { a: 123, b: x => true } is marked non-inferable because it contains a context sensitive | ||
// arrow function, but is considered partially inferable because property 'a' has an inferable type. | ||
function isPartiallyInferableType(type: Type): boolean { | ||
return !(getObjectFlags(type) & ObjectFlags.NonInferrableType) || | ||
isObjectLiteralType(type) && some(getPropertiesOfType(type), prop => isPartiallyInferableType(getTypeOfSymbol(prop))) || | ||
isTupleType(type) && some(getTypeArguments(type), isPartiallyInferableType); | ||
} | ||
|
||
function createReverseMappedType(source: Type, target: MappedType, constraint: IndexType) { | ||
// We consider a source type reverse mappable if it has a string index signature or if | ||
// it has one or more properties and is of a partially inferable type. | ||
if (!(getIndexInfoOfType(source, stringType) || getPropertiesOfType(source).length !== 0 && isPartiallyInferableType(source))) { | ||
// it has one or more properties | ||
if (!getIndexInfoOfType(source, stringType) && !getPropertiesOfType(source).length) { | ||
return undefined; | ||
} | ||
// For arrays and tuples we infer new arrays and tuples where the reverse mapping has been | ||
|
@@ -24121,6 +24111,19 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { | |
const templateType = getTemplateTypeFromMappedType(target); | ||
const inference = createInferenceInfo(typeParameter); | ||
inferTypes([inference], sourceType, templateType); | ||
const sourceValueDeclaration = sourceType.symbol?.valueDeclaration; | ||
if (sourceValueDeclaration) { | ||
const intraExpressionInferenceSites = getInferenceContext(sourceValueDeclaration)?.intraExpressionInferenceSites?.filter(site => isNodeDescendantOf(site.node, sourceValueDeclaration)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The The goal here is to only try to infer from the intra expression inference sites relevant for this specific reverse mapped symbol. Perhaps the simplest and good enough solution would be to get an appropriate trailing slice of all They are "aggregated" throughout the call to declare function f<T>(arg: {
produce: (n: string) => T;
consume: (x: T) => void;
}): void;
f({
produce: (n) => n,
consume: (x) => x.toLowerCase(), // ok, `x` is inferred
});
f({
consume: (x) => x.toLowerCase(), // doesn't work, 'x' is of type 'unknown'.(18046)
produce: (n) => n,
}); And thus, by extension... if we'd look for "relevant" nodes from the end we could slice the trailing elements until we meet one that is not relevant. Because intra intra expression inference sites are dependent on source order this should work just fine as it should be guaranteed that by using such a trailing slice we use all current "relevant" nodes and nothing else There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But doesn't this all just imply we should store intra-expression inference sites in a per-property map or something? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do u mean properties of the reverse mapped type? IIRC we might not know if a property is going to end up as property of such - or I don't know how to check that. So I don't know how to aggregate such a structure since I don't know when it should be created in the first place. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Since I opened this PR I got a way better understanding of how the whole intra-expression inference works. What is in this PR right now isn't ideal on the high level of things since it comes with a perf penalty cost. I'm trying to figure out how to best store those sites to accommodate the needs of this change and I'm hitting the wall. Some things to understand about this inference type:
So I'm looking for some way to store/read/clear those sites without hitting any of the mentioned drawbacks and for two different/similar-ush purposes but I can't figure this out so far. cc @jakebailey |
||
if (intraExpressionInferenceSites?.length) { | ||
const templateType = (getApparentTypeOfContextualType(sourceValueDeclaration.parent.parent as Expression, ContextFlags.NoConstraints) as MappedType).templateType; | ||
if (templateType) { | ||
Debug.assert(isExpressionNode(sourceValueDeclaration)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not fond of the cast to I'm not sure what's the codebase policy around stuff like that so if you have any suggestions on how this should be done here I'm all ears There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you're looking for Debug.assertNode? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for the tip, I pushed out a change to use it |
||
pushContextualType(sourceValueDeclaration as any as Expression, templateType, /*isCache*/ false); | ||
inferFromIntraExpressionSites([inference], intraExpressionInferenceSites); | ||
popContextualType(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. At first this felt quite hacky but now I'm leaning towards saying that it's the easiest and cleanest solution to this problem. The problem that this is solving (together with the I wasn't sure what value I should pass as |
||
} | ||
} | ||
} | ||
return getTypeFromInference(inference) || unknownType; | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
tests/cases/conformance/types/typeRelationships/typeInference/intraExpressionInferencesReverseMappedTypes.ts(67,21): error TS18046: 'x' is of type 'unknown'. | ||
tests/cases/conformance/types/typeRelationships/typeInference/intraExpressionInferencesReverseMappedTypes.ts(71,21): error TS18046: 'x' is of type 'unknown'. | ||
tests/cases/conformance/types/typeRelationships/typeInference/intraExpressionInferencesReverseMappedTypes.ts(80,21): error TS18046: 'x' is of type 'unknown'. | ||
tests/cases/conformance/types/typeRelationships/typeInference/intraExpressionInferencesReverseMappedTypes.ts(86,21): error TS18046: 'x' is of type 'unknown'. | ||
tests/cases/conformance/types/typeRelationships/typeInference/intraExpressionInferencesReverseMappedTypes.ts(95,21): error TS18046: 'x' is of type 'unknown'. | ||
tests/cases/conformance/types/typeRelationships/typeInference/intraExpressionInferencesReverseMappedTypes.ts(101,21): error TS18046: 'x' is of type 'unknown'. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All of those relate to the tuple-based tests that I added and those are not expected. It doesn't work because in tuples both With objects I could try to fix this in this PR but I also feel like this would require changes that could be moved to a separate followup PR. This PR could focus on the base mechanism for this improvement and on object-based cases. |
||
|
||
|
||
==== tests/cases/conformance/types/typeRelationships/typeInference/intraExpressionInferencesReverseMappedTypes.ts (6 errors) ==== | ||
// repro cases based on https://github.com/microsoft/TypeScript/issues/53018 | ||
|
||
declare function f<T>( | ||
arg: { | ||
[K in keyof T]: { | ||
produce: (n: string) => T[K]; | ||
consume: (x: T[K]) => void; | ||
}; | ||
} | ||
): T; | ||
|
||
const res1 = f({ | ||
a: { | ||
produce: (n) => n, | ||
consume: (x) => x.toLowerCase(), | ||
}, | ||
b: { | ||
produce: (n) => ({ v: n }), | ||
consume: (x) => x.v.toLowerCase(), | ||
}, | ||
}); | ||
|
||
const res2 = f({ | ||
a: { | ||
produce: function () { | ||
return "hello"; | ||
}, | ||
consume: (x) => x.toLowerCase(), | ||
}, | ||
b: { | ||
produce: function () { | ||
return { v: "hello" }; | ||
}, | ||
consume: (x) => x.v.toLowerCase(), | ||
}, | ||
}); | ||
|
||
const res3 = f({ | ||
a: { | ||
produce() { | ||
return "hello"; | ||
}, | ||
consume: (x) => x.toLowerCase(), | ||
}, | ||
b: { | ||
produce() { | ||
return { v: "hello" }; | ||
}, | ||
consume: (x) => x.v.toLowerCase(), | ||
}, | ||
}); | ||
|
||
declare function f2<T extends unknown[]>( | ||
arg: [ | ||
...{ | ||
[K in keyof T]: { | ||
produce: (n: string) => T[K]; | ||
consume: (x: T[K]) => void; | ||
}; | ||
} | ||
] | ||
): T; | ||
|
||
const res4 = f2([ | ||
{ | ||
produce: (n) => n, | ||
consume: (x) => x.toLowerCase(), | ||
~ | ||
!!! error TS18046: 'x' is of type 'unknown'. | ||
}, | ||
{ | ||
produce: (n) => ({ v: n }), | ||
consume: (x) => x.v.toLowerCase(), | ||
~ | ||
!!! error TS18046: 'x' is of type 'unknown'. | ||
}, | ||
]); | ||
|
||
const res5 = f2([ | ||
{ | ||
produce: function () { | ||
return "hello"; | ||
}, | ||
consume: (x) => x.toLowerCase(), | ||
~ | ||
!!! error TS18046: 'x' is of type 'unknown'. | ||
}, | ||
{ | ||
produce: function () { | ||
return { v: "hello" }; | ||
}, | ||
consume: (x) => x.v.toLowerCase(), | ||
~ | ||
!!! error TS18046: 'x' is of type 'unknown'. | ||
}, | ||
]); | ||
|
||
const res6 = f2([ | ||
{ | ||
produce() { | ||
return "hello"; | ||
}, | ||
consume: (x) => x.toLowerCase(), | ||
~ | ||
!!! error TS18046: 'x' is of type 'unknown'. | ||
}, | ||
{ | ||
produce() { | ||
return { v: "hello" }; | ||
}, | ||
consume: (x) => x.v.toLowerCase(), | ||
~ | ||
!!! error TS18046: 'x' is of type 'unknown'. | ||
}, | ||
]); | ||
|
||
declare function f3<T>( | ||
arg: { | ||
[K in keyof T]: { | ||
other: number, | ||
produce: (n: string) => T[K]; | ||
consume: (x: T[K]) => void; | ||
}; | ||
} | ||
): T; | ||
|
||
const res7 = f3({ | ||
a: { | ||
other: 42, | ||
produce: (n) => n, | ||
consume: (x) => x.toLowerCase(), | ||
}, | ||
b: { | ||
other: 100, | ||
produce: (n) => ({ v: n }), | ||
consume: (x) => x.v.toLowerCase(), | ||
}, | ||
}); | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I expect that this part is not fully correct. What I'd like to do here is to only get
sourceValueDeclaration
if it's an anonymous/fresh/smth declaration within within the argument position. I'm not sure how to check for this.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use
findAncestor
to check for argument position membership, useObjectFlags.FreshLiteral
to check for freshness on the type.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added
.valueDeclaration
to reverse mapped properties. This grants me clean access to the node I'm looking for and fixes an issue with array literals used as values of object reverse mapped types (test case). I think that this gives me now exactly what I'm looking for when combined with the freshness check and I don't need to do anyfindAncestor
traversal.