Skip to content

Conversation

sandersn
Copy link
Member

@sandersn sandersn commented Oct 30, 2018

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:

  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.

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:

function C(config) {
  if (config === undefined) config = {};
  this.x = config.x;
  this.y = config.y;
  this.z = config.z;
}

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.

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;
}
```
@sandersn sandersn requested review from a user and PranavSenthilnathan October 30, 2018 23:19
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);
Copy link

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?

@ghost
Copy link

ghost commented Oct 31, 2018

This can be done by combining inferFromCandidates with inferFromStructure. This also makes checker.mapType unnecessary because we can just keep the candidates as an array instead of creating a union type.

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;
}

@sandersn
Copy link
Member Author

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.
@sandersn
Copy link
Member Author

I think this is ready to look at again. I updated the PR description to reflect the new commits.

@ghost
Copy link

ghost commented Oct 31, 2018

We can avoid some state in usageContext by computing flags at the end:

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) => {
Copy link

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.

Copy link
Member Author

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.

@sandersn
Copy link
Member Author

That's how I had it, but at one point I had four variables, four calls to some and three parameters to isVacuousType. Things have calmed down since then, so I think your suggestion makes sense. However, I may end up re-making this change if more properties need to be passed around.

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.
@sandersn sandersn merged commit 8056e2b into master Nov 2, 2018
@sandersn sandersn deleted the infer-from-usage/create-inference-passes branch November 2, 2018 16:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant