Skip to content

Type narrowing based on function typeof is broken when function is intersected with a record #45801

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

Closed
safareli opened this issue Sep 9, 2021 · 1 comment Β· Fixed by #47483
Closed
Assignees
Labels
Bug A bug in TypeScript Fix Available A PR has been opened for this issue Rescheduled This issue was previously scheduled to an earlier milestone

Comments

@safareli
Copy link

safareli commented Sep 9, 2021

Bug Report

πŸ”Ž Search Terms

type narrowing based on function typeof is broken when intersection with a record is used

πŸ•— Version & Regression Information

  • Recent change in behaviour happened in version 4.3.5

⏯ Playground Link

Playground link with relevant code

πŸ’» Code

type Meta = { foo: string }
interface F { (): string}
const x = (a: (F & Meta) | string) => {
    if (typeof a === "function"){
        // ts.version >= 4.3.5: never -- unexpected
        // ts.version <= 4.2.3: F & Meta -- expected
        a;
    } else {
        // expected: string
        // ts.version >= 4.3.5: string -- expected
        // ts.version <= 4.2.3: string | (F & Meta) -- unexpected
        a;
    }
}

πŸ™ Actual behavior

behavior was half correct from in TS versions <= 4.2.3, but then it was changed in 4.3.5 which fixed the part that was wrong previously, but broke the other part that was correct. So we still have half broken behavior.

πŸ™‚ Expected behavior

It's expected to pick the correct parts from the both versions.

@andrewbranch andrewbranch added the Bug A bug in TypeScript label Sep 10, 2021
@andrewbranch andrewbranch added this to the TypeScript 4.5.1 milestone Sep 10, 2021
@a-tarasyuk
Copy link
Contributor

It's a little bit tricky case. The previous versions used | for Union and Intersection types

function getTypeFactsOfTypes(types: Type[]): TypeFacts {
    let result: TypeFacts = TypeFacts.None;
    for (const t of types) {
        result |= getTypeFacts(t);
    }
    return result;
}

Currently, TS separates Union and Intersection. To get a type in if block, all types should follow the same flags

interface A { (): number; }
interface B { (): string; }
interface C { (): void; }

const x = (a: (A & B & C) | string) => {
    if (typeof a === "function"){
        a; // A & B ((FunctionFacts & FunctionFacts) & TypeofEQFunction
)
    }
    else {
        a; // string 
    }
}

Or to be empty

interface A { (): number; }
type B = {};

const x = (a: (A & B) | string) => {
    if (typeof a === "function"){
        a; // A
)
    }
    else {
        a; // string 
    }
}
if (flags & TypeFlags.Intersection) {
    // When an intersection contains a primitive type we ignore object type constituents as they are
    // presumably type tags. For example, in string & { __kind__: "name" } we ignore the object type.
    ignoreObjects ||= maybeTypeOfKind(type, TypeFlags.Primitive);
    return reduceLeft((type as UnionType).types, (facts, t) => facts & getTypeFacts(t, ignoreObjects), TypeFacts.All);
}

I'm not sure which fix is appropriate for such cases. Should the TS allow a intersection with the non-empty object and functions?

/cc @andrewbranch

@RyanCavanaugh RyanCavanaugh added the Rescheduled This issue was previously scheduled to an earlier milestone label Dec 15, 2021
@typescript-bot typescript-bot added the Fix Available A PR has been opened for this issue label Dec 31, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript Fix Available A PR has been opened for this issue Rescheduled This issue was previously scheduled to an earlier milestone
Projects
None yet
6 participants