Skip to content

Type guard doesn't narrow out union member, for only some purposes #52549

Closed
@wbt

Description

@wbt

Bug Report

🔎 Search Terms

type guard const narrow narrowing union

🕗 Version & Regression Information

  • The comment "no TS error" became true between 4.2.3 and 4.3.5. That fix may be instructive, and possibly broadenable to work when used as a type parameter.
  • The errors became a bit more sensible between versions 4.5.5 and 4.6.4.
  • Changes related to the Lowercase example of tying input and output types together are not reported here because the comment still holds.
  • Nightly version at time of test: v5.0.0-dev.20230201

⏯ Playground Link

Playground link with relevant code

💻 Code

type G7Abbreviation = 'CA' | 'FR' | 'DE' | 'IT' | 'JP' | 'UK' | 'US' | 'EU';
type G8Abbreviation = G7Abbreviation | 'RU';
declare function takesG7WithParam<A extends G7Abbreviation>(arg0: A) : void;
declare function takesG7WithoutParam(arg0: G7Abbreviation) : void;
const takesG8 = function<
    TN extends G8Abbreviation //Constrains other params/return type in inspiring example; type param required
>(abbreviation: TN) : Lowercase<TN> {
    const abbreviationNarrowed = abbreviation; //type: G8Abbreviation at broadest
    if(abbreviationNarrowed === 'RU') {
        console.log('Legacy case handling, not passed on to next function.');
    } else {
        //This type should be Exclude<TN, 'RU'>, at most Exclude<G8Abbreviation, 'RU'>,
        //which in this case happens to be at its broadest G7Abbreviation
        const abbreviationNarrowedAgain = abbreviationNarrowed;
        //Error 2344: Type '"RU"' is not assignable to type 'G7Abbreviation'.
        takesG7WithParam<typeof abbreviationNarrowed>(abbreviationNarrowed);
        //Error 2345: Same 1st line as above error but without explanation lines:
        takesG7WithParam<typeof abbreviationNarrowedAgain>(abbreviationNarrowedAgain);
        takesG7WithParam<typeof abbreviation>(abbreviation); // same as above line
        //Next two lines should work the same, but first has error 2345:
        //'G8Abbreviation' is not assignable to 'G7Abbreviation'.
        takesG7WithoutParam(abbreviationNarrowedAgain);
        takesG7WithoutParam(abbreviationNarrowed); //no TS error, but type params are sometimes required!
        takesG7WithoutParam(abbreviation); //error because parameters aren't narrowed, not the Issue here
    }
    return abbreviation.toLowerCase() as Lowercase<TN>;
}

Note that a call to takesG7WithParam specifying the type parameter as the full union G7Abbreviation with the parameter abbreviationNarrowed works, but sometimes the type parameter should be typed more narrowly (and maybe there could be an issue on whether the parameter is assignable to that, but typeof with the same variable name should help.)

This demonstration doesn’t appear to be affected by known issues with conditional types, nor does it deal with function parameters or ArrayBuffer/Uint8Array types.

🙁 Actual behavior

Const types are not narrowed as expected, except that abbreviationNarrowed is correctly narrowed for the purpose of assigning type to a value parameter (its typeof result is not narrowed for the purpose of use as a type parameter though).

🙂 Expected behavior

  • The assignment of one constant to another (here, to abbreviationNarrowedAgain) does not lead to a new error (e.g. the difference between the first two calls to takesG7WithoutParam).
  • Both of the consts are narrowed to Exclude<G8Abbreviation, 'RU'> within the else block, for all purposes.
  • None of the calls involving abbreviationNarrowed or abbreviationNarrowedAgain have errors.
  • All of the errors should have the additional lines of detail shown on the first one, but only the version of the first error from TS 4.6+. (Worse errors in older versions is not raised as an issue here with the current version.)
  • When the type of a value is narrowed for the purpose of use as a value parameter, its typeof is also narrowed for the purpose of use as a type parameter.
  • The two declared functions should be equivalent, except that the one with a type parameter is more readily extensible to add other parameters which are connected to the first one through the generic type parameter.
  • Also in the category of expected behavior, the last line shouldn't require a cast, but that's a separate issue (String case change methods should return intrinsic string manipulation types #44268) not core to the issue being raised here.
  • Per comment by @MartinJohns below, the error in calls to takesG7WithoutParam should note that TN is the type that can't be assigned to G7Abbreviation rather than G8Abbreviation.

Metadata

Metadata

Assignees

No one assigned

    Labels

    DuplicateAn existing issue was already created

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions