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

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

Closed
jcalz opened this issue Apr 18, 2023 · 5 comments · Fixed by #53907
Closed

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

jcalz opened this issue Apr 18, 2023 · 5 comments · Fixed by #53907
Labels
Bug A bug in TypeScript Help Wanted You can do this
Milestone

Comments

@jcalz
Copy link
Contributor

jcalz commented Apr 18, 2023

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).

@fatcerberus
Copy link

fatcerberus commented Apr 18, 2023

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)

@jcalz
Copy link
Contributor Author

jcalz commented Apr 18, 2023

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.

@fatcerberus
Copy link

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.

@jcalz
Copy link
Contributor Author

jcalz commented Apr 18, 2023

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

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Apr 18, 2023

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript Help Wanted You can do this
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants