Skip to content
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

Subtype narrowing bug with falsey types (''/0/[]) #39105

Closed
chancancode opened this issue Jun 17, 2020 · 4 comments · Fixed by #49625
Closed

Subtype narrowing bug with falsey types (''/0/[]) #39105

chancancode opened this issue Jun 17, 2020 · 4 comments · Fixed by #49625
Labels
Bug A bug in TypeScript
Milestone

Comments

@chancancode
Copy link
Contributor

TypeScript Version: 3.9.2

Search Terms: some combination of subtype, narrowing, type assertion, undefined, null, empty string, empty tuple

Code

declare function isEmptyString(value: string): value is '';
declare function isMaybeEmptyString(value: string | null | undefined): value is '' | null | undefined;

declare function isZero(value: number): value is 0;
declare function isMaybeZero(value: number | null | undefined): value is 0 | null | undefined;

declare function isEmptyArray<T>(value: T[]): value is [];
declare function isMaybeEmptyArray<T>(value: T[] | null | undefined): value is [] | null | undefined;

const TEST_CASES = [
    (value: string) => {
        if (isEmptyString(value)) {
            value // ""
        } else {
            value // string
        }

        if (isMaybeEmptyString(value)) {
            value // ""
        } else {
            value // string
        }
    },

    (value?: string) => {
        if (isMaybeEmptyString(value)) {
            value // EXPECTED '' | undefined ; ACTUAL undefined
        } else {
            value // string
        }
    },

    (value: number) => {
        if (isZero(value)) {
            value // 0
        } else {
            value // number
        }

        if (isMaybeZero(value)) {
            value // 0
        } else {
            value // number
        }
    },

    (value?: number) => {
        if (isMaybeZero(value)) {
            value // EXPECTED 0 | undefined ; ACTUAL undefined
        } else {
            value // number
        }
    },

    (value: string[]) => {
        if (isEmptyArray(value)) {
            value // []
        } else {
            value // string[]
        }

        if (isMaybeEmptyArray(value)) {
            value // EXPECTED [] ; ACTUAL string[] & [] (GOOD ENOUGH but why is it different than the above?)
        } else {
            value // string[]
        }
    },

    (value?: string[]) => {
        if (isMaybeEmptyArray(value)) {
            value // EXPECTED [] | undefined ; ACTUAL undefined
        } else {
            value // string[]
        }
    },
];

Expected behavior: (inlined)

Actual behavior: (inlined)

TL;DR the type system is apparently expressive enough to express this, until null/undefined got involved

Playground Link: https://www.typescriptlang.org/play/index.html?ssl=1&ssc=1&pln=76&pc=3#code/CYUwxgNghgTiAEAzArgOzAFwJYHtXywGcBRAWwAcMBPAZQxi1QHMAKANygmRAC55D6jJgEo+HLgiLwA5NIDcAKFCRYCFOmx4ChALJQqAIxBlKtQc3adufAQ2bwAPvFTIIER-DShEjEMFHw4tzaMtIeLm4eXiA+qH6KSuDQcEhomLj4RABaIDA4lhJ8LqRGMAFBkoTwAAyKyslqaZqZuvpGOXkF1s7IJbnhru5O0bF+5VaVNQORw6jevsAJ9aqpGhnaJtQAgjAw+gA8ACoAfF288IcA2gC64xIhN3VJK+rpWkR6hsYU27sHJ2c+FdrtMhp45jEFndglIbqCohDRosFAowHgBBdiDRDgB9ADCWxoWPgAF54JcFPAqfBAfxzCJScd4ABvSnU9lYRA0oibMx2VgVYTCFls9liwITeAAeil8AARHLRWKAL7wEAQQgIVni8UVaWy2xCJXs5UonUELksD5tb6mOj8s5CkXm9l6mXyxXm1XqzXOl1Ut0G+nG6mmk0AGjN1LOAH4bPThSSmdqdZzua0vrz7UJHcKU-7A-BiAANAAKxDxh2IABFQgj5nFgPA5PAtpWAKpbAAy4IbfhDVO9Gq1A7FhcNzFHYdDkaVtOKpUTydHaathA6+UFedHrsl7uqU7Vw79Bb3soXuSnUfFq+tXw3uZPLsLB69R99+efZ56fRgV4j140hUcY-oujJPhylp3u0uSbhMTqfuahYluWlY1lMsx9k2LZtocnY9iMCyHj6I7+gG34Xn+XpKsqs7srSE5MDcS4QdSt4kD8VA7HsVCPohOqFjcxHHvxurfoxQnUSuUEZkYvLcfofE7tSyFlhWVa1nCOEdt2dL8nCABk5IgiwADiADy5m1sQABy5ntqZAAS8AGMgGDwAA7gAFlQIRYO5wCcoguQgKg7kYF5UD4BFCBQAYOBsCAMbCMJH7KeR9zuhJ1z-jOgGxvG+m3OBokWumnxyZxCm8VurFiZlsooep6FwphkKNs2rY6QRiJEW+JF1WO4n0pJOrToOkbXHIQA

Related Issues:

#18196
#18413
#27103
#27157

Maybe same as #31156, not 100% sure

@chancancode
Copy link
Contributor Author

I am guessing it probably has something to do with:

let a!: string | null;

if (a) {
    a // string
} else {
    a // string | null
}

let b!: string;

if (b) {
    b // string
} else {
    b // string
}

@RyanCavanaugh RyanCavanaugh added the Bug A bug in TypeScript label Jun 17, 2020
@RyanCavanaugh RyanCavanaugh added this to the Backlog milestone Jun 17, 2020
@RyanCavanaugh
Copy link
Member

This isn't specific to null/undefined; you can see similar behavior in this example

declare type Animal = { move: any };
declare type Cat = Animal & { meow: any };
declare function IsZeroOrCat(c: any): c is Cat | 0;

declare const sc: string | Animal;
if (IsZeroOrCat(sc)) {
    sc // Should just be 'Cat'
}

@chancancode chancancode changed the title Subtype narrowing bug with null/undefined Subtype narrowing bug with falsey types (''/0/[]) Jun 18, 2020
@chancancode
Copy link
Contributor Author

@RyanCavanaugh thanks for confirming. I modified the title, although I'm not 100% sure I understand the cause of this bug correctly at this point.

@jack-williams
Copy link
Collaborator

jack-williams commented Jun 26, 2020

Looks like a manifestation of #31156

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript
Projects
None yet
3 participants