Microsoft/TypeScript

Infer from usage type logic

Open

#52,074 创建于 2023年1月2日

在 GitHub 查看
 (2 评论) (0 反应) (0 负责人)TypeScript (6,726 fork)batch import
Experience EnhancementHelp WantedSuggestion

仓库指标

Star
 (48,455 star)
PR 合并指标
 (平均合并 6天 17小时) (30 天内合并 9 个 PR)

描述

Bug Report

🔎 Search Terms

  • infer from usage
  • interface higher priority then type on type inference

🕗 Version & Regression Information

I think this has always been the behavior as far as I know. I didn't play around with this much yet, but the behavior exists in 4.0.5 and nightly and it seems odd for it to bounce around a lot.

⏯ Playground Link

playground link

💻 Code



// infers -> : { x: any; } = this makes sense
function inferWorksFine(obj) {
    return obj.x
}

// infers -> Z, this also makes sense
function inferFromFunctionCallTypedWithType(obj) {
    typedWithType(obj)
}
// infers -> Q, also makes sense
function inferFromFunctionCallTypedWithInterface(obj) {
    typedWithInterface(obj)
}

/*
 *  Here is when behavior gets odd
 * 
 */

// infers -> { toplevel?: any; z?: number }
//      I would prefer if toplevel resovled to a non ? type but thats sorta stylistic
//      z is not optional and making it results in a type error
function inferFromUsageAndFunctionCallTypedWithType(obj) {
    typedWithType(obj)
    return obj.toplevel
}
// infers -> Q 
//      This seems wrong to me, interface "types" have priority over 
//      anonymous types, this results in errors because the inferred property 
//      is an anonymous type
function inferFromUsageAndFunctionCallTypeWithInterface(obj) {
    typedWithInterface(obj)
    return obj.toplevel
}
// infers -> Q
//      From what I understand. The statement "type Z = {z: number}" 
//      is a type alias Z to an anonymous type {z: number} so the interface 
//      still has priority over it. 
function inferFromFunctionCallTypedWithTypeAndFunctionCallTypedWithInterface(obj) {
    typedWithType(obj)
    typedWithInterface(obj)
}

// There are more examples you could contrive that look like this/combinations of this
// but I think this is enough for the point




type Z = {z: number}
function typedWithType(z: Z) {

}

interface Q {
    q: number
}
function typedWithInterface(q: Q) {}

🙁 Explanation of Actual behavior

Note: If I reference line numbers or any function I'm referring to the main branch I pulled I think yesterday.

So I sort of already discussed the behavior in the code itself. So I think it makes sense to talk about the code that causes this behavior. So this file path "TypeScript/src/services/codefixes/inferFromUsage.ts" is where the logic for inferFromUsage lives afaik.

Just to do this in order:

// infers -> { toplevel?: any; z?: number }
//      I would prefer if toplevel resovled to a non ? type but thats sorta stylistic
//      z is not optional and making it results in a type error
function inferFromUsageAndFunctionCallTypedWithType(obj) {
    typedWithType(obj)
    return obj.toplevel
}

The reason these are optional seem to stem from the function combineAnonymousTypes Specifically

const members = mapEntries(props, (name, types) => {
            const isOptional = types.length < anons.length ? SymbolFlags.Optional : 0;
            const s = checker.createSymbol(SymbolFlags.Property | isOptional, name as __String);
            s.links.type = checker.getUnionType(types);
            return [name, s];
        });

I'm actually not sure what the isOptional is intended to do. From what I understand anons.length is the number of anonymous types that the function is passed while types refers to possible type resolutions of the property with name name. I'm not sure what comparing these means.

Priority of interfaces

Since in the code I mention why I think type aliasing doesn't change anything I'll just show one example.

function inferFromFunctionCallTypedWithTypeAndFunctionCallTypedWithInterface(obj) {
    typedWithType(obj)
    typedWithInterface(obj)
}

So here the type will be resolved to the interface type. In the function combineTypes there is a priority table defined as follows:

const priorities: Priority[] = [
            {
                high: t => t === checker.getStringType() || t === checker.getNumberType(),
                low: t => t === stringNumber
            },
            {
                high: t => !(t.flags & (TypeFlags.Any | TypeFlags.Void)),
                low: t => !!(t.flags & (TypeFlags.Any | TypeFlags.Void))
            },
            {
                high: t => !(t.flags & (TypeFlags.Nullable | TypeFlags.Any | TypeFlags.Void)) && !(getObjectFlags(t) & ObjectFlags.Anonymous),
                low: t => !!(getObjectFlags(t) & ObjectFlags.Anonymous)
            }];

The last entry

{
                high: t => !(t.flags & (TypeFlags.Nullable | TypeFlags.Any | TypeFlags.Void)) && !(getObjectFlags(t) & ObjectFlags.Anonymous),
                low: t => !!(getObjectFlags(t) & ObjectFlags.Anonymous)
            }

gives high priority to non-anonymous types which I think only means interfaces or a type which at some point was mixed with an interface? This would take further investigation. I'm not sure the purpose of this behavior or if its a bug.

🙂 Expected behavior

I want the behavior to be that the generated types don't cause errors when they don't have to, since currently generated types don't satisfy what they need to be.

Deleting these two lines "solves" the behavior, but there are definitely more things to look into. I'm not sure why they were written - like I don't have any intuition on what problem they solve - but I'm pretty sure they are there for important behavior.

Also since I'm listing things, I think this is the other "big one"

There are also more cases like

function f(obj) {
    g(obj)
    return obj.v > 1
}

not being able to resolve v to a number if "g" expects some type (even if the type is unrelated like {q}) and others.

贡献者指南