-
Notifications
You must be signed in to change notification settings - Fork 13k
infer from usage's unification uses multiple passes #28244
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
Conversation
Previously, the unification step of infer-from-usage codefix would stop as soon an answer was found. Now it continues if the result is *incomplete*, with the idea that later passes may provide a better inference. Currently, an *incomplete* inference is 1. The type any. 2. The empty object type `{}` or a union or intersection that contains `{}`. In the checker, any takes priority over other types since it basically shuts down type checking. For type inference, however, any is one of the least useful inferences. `{}` is not a good inference for a similar reason; as a parameter inference, it doesn't tell the caller much about what is expected, and it doesn't allow the function author to use an object as expected. But currently it's inferred whenever there's an initialisation with the value `{}`. With this change, subsequent property assignments to the same parameter will replace the `{}` with a specific anonymous type. For example: ```js function C(config) { if (config === undefined) config = {}; this.x = config.x; this.y = config.y; this.z = config.z; } ```
return undefined; | ||
const structuralInference = checker.createAnonymousType(/*symbol*/ undefined!, members, callSignatures, constructSignatures, stringIndexInfo, /*numberIndexInfo*/ undefined); // TODO: GH#18217 | ||
if (previous && checker.isEmptyObjectType(previous)) { | ||
return checker.mapType(previous, t => checker.isEmptyObjectType(t) ? structuralInference : t); |
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 had to stare at this for a while until I realized isEmptyObjectTypes
returns true non-empty non-object types like 42 | {}
. And mapType
has nothing to do with mapped types, why on earth would I think that?
This can be done by combining function getTypeFromUsageContext(usageContext: UsageContext, checker: TypeChecker): Type | undefined {
return inferFromPrimitive(usageContext, checker) ||
inferFromObject(usageContext, checker) ||
inferFromStructure(usageContext, checker);
}
function inferFromStructure(usageContext: UsageContext, checker: TypeChecker): Type | undefined {
let propertiesAreOptional = false;
const candidates = mapDefined(usageContext.candidateTypes, t => {
const type = checker.getBaseTypeOfLiteralType(t);
if (checker.isEmptyObjectType(type)) {
propertiesAreOptional = true;
return undefined;
}
return type;
});
if (usageContext.numberIndexContext) {
return checker.createArrayType(recur(usageContext.numberIndexContext));
}
else if (usageContext.properties || usageContext.callContexts || usageContext.constructContexts || usageContext.stringIndexContext) {
const optionalFlag = propertiesAreOptional ? SymbolFlags.Optional : 0;
const members = mapValues(usageContext.properties, (context, name) => {
const symbol = checker.createSymbol(SymbolFlags.Property | optionalFlag, name);
symbol.type = recur(context);
return symbol;
});
const callSignatures = map(usageContext.callContexts, callContext => getSignatureFromCallContext(callContext, checker)) || emptyArray;
const constructSignatures = map(usageContext.constructContexts, constructContext => getSignatureFromCallContext(constructContext, checker)) || emptyArray;
const stringIndexInfo = usageContext.stringIndexContext && checker.createIndexInfo(recur(usageContext.stringIndexContext), /*isReadonly*/ false);
const structuralInference = checker.createAnonymousType(/*symbol*/ undefined!, members, callSignatures, constructSignatures, stringIndexInfo, /*numberIndexInfo*/ undefined); // TODO: GH#18217
return checker.getWidenedType(checker.getUnionType([...candidates, structuralInference]));
}
else {
return candidates.length ? checker.getWidenedType(checker.getUnionType(candidates)) : undefined;
}
function recur(innerContext: UsageContext): Type {
return getTypeFromUsageContext(innerContext, checker) || checker.getAnyType();
}
}
function mapValues<T, U>(map: ReadonlyUnderscoreEscapedMap<T> | undefined, cb: (t: T, key: __String) => U): UnderscoreEscapedMap<U> {
if (!map) return emptyMap as any as UnderscoreEscapedMap<U>;
const res = createUnderscoreEscapedMap<U>();
map.forEach((value, key) => res.set(key, cb(value, key)));
return res;
} |
Yeah, I realized nearly the same thing on the walk home. I’ll improve it tomorrow morning. |
In the previous commit, I changed inference from usage to continue inference if a the result was *incomplete*. This commit now runs all 4 inference passes and combines them in a unification step. Currently the unification step is simple, it: 1. Gathers all inferences in a list. 2. Makes properties of anonymous types optional if there is an empty object in the inference list. 3. Removes *vacuous* inferences. 4. Combines the type in a union. An inference is *vacuous* if it: 1. Is any or void, when a non-any, non-void type is also inferred. 2. Is the empty object type, when an object type that is not empty is also inferred. 3. Is an anonymous type, when a non-nullable, non-any, non-void, non-anonymous type is also inferred. I think I might eventually want a concept of priorities, like the compiler's type parameter inference, but I don't have enough examples to be sure yet. Eventually, unification should have an additional step that examines the whole inference list to see if its contents are collectively meaningless. A good example is `null | undefined`, which is not useful.
@Andy-MS pointed out that my empty object code was a special case of merging all anonymous types from an inference and making properties optional that are not in all the anonymous type. So I did that instead.
I think this is ready to look at again. I updated the PR description to reflect the new commits. |
We can avoid some state in const inferences = [fallback, ...inferFromPrimitive(k, checker), ...inferFromContext(k, checker), ...inferFromObject(k, checker), ...inferFromStructure(k, checker)];
const hasNonVacuousType = inferences.some(type => !(type.flags & (TypeFlags.Any | TypeFlags.Void)));
const hasNonVacuousNonAnonymousType = inferences.some(type =>
!(type.flags & (TypeFlags.Nullable | TypeFlags.Any | TypeFlags.Void)) && !(checker.getObjectFlags(type) & ObjectFlags.Anonymous)); |
numberIndexReadonly = numberIndexReadonly || anon.numberIndexInfo.isReadonly; | ||
} | ||
} | ||
const members = mapEntries(props, (name, types) => { |
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.
Could use some tests of combining different object inferences.
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 one, and now I'm taking a look at the user tests for more examples.
That's how I had it, but at one point I had four variables, four calls to |
Because they still have a separate code path, they didn't use the new unification code. Also some cleanup from PR comments.
Also remove dead code.
Instead of relying on the unification code to remove the fallback.
Previously,
getTypeFromUsageContext
of the infer-from-usage codefix would stop as soon an answer was found. Now it runs all 4 inference passes and combines them in a unification step. Currently the unification step is simple, it:object in the inference list.
An inference is vacuous if it:
In the checker, any takes priority over other types since it basically shuts down type checking. For type inference, however, any is one of the least useful inferences. I think I might eventually want a concept of priorities, like the compiler's type parameter inference, but I don't have enough examples to be sure yet.
{}
is not a good inference for a similar reason; as a parameter inference, it doesn't tell the caller much about what is expected, and it doesn't allow the function author to use an object as expected. But currently it's inferred whenever there's an initialisation with the value{}
. With this change, subsequent property inferences to the same parameter will replace the{}
with a specific anonymous type. For example:Should produce
{ x?, y?, z? } | undefined
not{} | undefined
.Eventually, unification should have an additional step that examines the whole inference list to see if its contents are collectively meaningless. A good example is
null | undefined
, which is not useful.