镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

Contextual typing fails when discriminant is an interpolated template literal #53888

Description

@jcalz

Bug Report

🔎 Search Terms

discriminated union, discriminant, interpolated template literal, contextual typing, delayed, deferred

🕗 Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about discriminated unions

⏯ Playground Link

Playground link with relevant code

💻 Code

type S = { d: "s"; cb: (x: string) => void; };
type N = { d: "n"; cb: (x: number) => void; };
declare function foo(foo: S | N): void;
foo({ d: "n", cb: x => x.toFixed() }); // okay
foo({ d: "s", cb: x => x.toUpperCase() }); // okay 
foo({ d: `${"s"}`, cb: x => x.toXXX() }); // error!
// ------------------> ~
// Parameter 'x' implicitly has an 'any' type.

🙁 Actual behavior

In the third call to foo(), the discriminant property d is an interpolated template literal whose type is correctly seen to be "s", but the cb property's callback parameter x is not contextually typed as string and instead implicitly falls back to any.

🙂 Expected behavior

The third call to foo() should behave just like the second call to foo(), where the object is narrowed to S and therefore cb's parameter is contextually typed as string.

Notes

Comes from this Stack Overflow question

I'm imagining this is just a design limitation where the type of d is computed too late for it to be available when the contextual typing for cb happens. A similar failure occurs when the discriminant is the output of an inline function call:

function justS(): "s" { return "s" };
foo({ d: justS(), cb: x => x.toXXX() }); // error!
// -----------------> ~
// Parameter 'x' implicitly has an 'any' type.

Playground link

I searched around for an existing issue but I didn't find an exact enough match. There's #35769 / #41759 / #46847, for example... related but not obviously the same.

The obvious workaround is just to do the interpolation ahead of time into a const and use that instead:

const s = `${"s"}` as const;
foo({ d: s, cb: x => x.toUpperCase() }); // okay

Playground link

Mostly I'm just looking for an official status on this (and expecting it to be Design Limitation).

Activity

  1. fatcerberus commented on Apr 18, 2023

    @fatcerberus

    This feels like one of those “you were trying to break it and succeeded” type deals that will likely not be considered worth fixing since it’s unlikely anyone would hit it by accident and/or be blocked by it (i.e. “Why would you write it this way?” ~ Future Ryan, probably)

  2. jcalz commented on Apr 18, 2023

    @jcalz
    ContributorAuthor

    This SO question was asked by someone who is apparently blocked by it; presumably their actual code is less contrived. I agree that it's probably not going to be considered worth addressing, though.

  3. fatcerberus commented on Apr 18, 2023

    @fatcerberus

    Hmm, while it doesn't invalidate the OP here, that SO question has this type:

    type BiomePlainLinkProps = {
      href: string;
      onClick?: (event: string) => void;
    }
    
    type BiomeButtonProps = {
      href?: never;
      onClick?: (event: number) => void;
    }
    
    type ClickableDiscriminatedUnion =
      | BiomePlainLinkProps
      | BiomeButtonProps;

    I'm not even sure that's a valid discriminated union? I thought the discriminant had to be a literal type (or at least a union of them), not just string.

  4. jcalz commented on Apr 18, 2023

    @jcalz
    ContributorAuthor

    The discriminant there is an optional undefined; you can test that it acts as a discriminated union for non-interpolated href

  5. RyanCavanaugh commented on Apr 18, 2023

    @RyanCavanaugh
    Member

    This seems wrong; the example as presented isn't any more difficult to resolve than this one:

    type S = { d: "s" | "t"; cb: (x: string) => void; };
    type N = { d: "n"; cb: (x: number) => void; };
    
    declare const dv: "s" | "t";
    foo({ d: dv, cb: x => x.toXXX() }); // does the right thing
  6. locked as resolved and limited conversation to collaborators on Oct 22, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugA bug in TypeScriptHelp WantedYou can do this

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions