microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
101k stars 12.48k forks source link

intersection type between two template literal type will never be 'never' type #60446

Open anatawa12 opened 1 day ago

anatawa12 commented 1 day ago

🔎 Search Terms

intersection template type never

🕗 Version & Regression Information

I think this is bug since template literal type is introduced in 4.1.

⏯ Playground Link

https://www.typescriptlang.org/play/?ts=5.8.0-dev.20241106#code/GYVwdgxgLglg9mABAQzATwBQEoBcL2IDeiUAFgE5wDuiYIANvQNyIC+AUOxAgM5QkBTPgFY8AAwC2MZCDIB6ACSE+5GGADmrORFICIAazGIAZIjFKVazXNgSB9NQKMBefJixN2cuYl++AegD8Xj5+ZDA8iDykcAwAJogARgK0AgBuAuRJsohxcEK0cFDsQA

💻 Code

function any(): any { throw null; }

const test5: `miauth/${string}/check` & `${string}/timeline` = any();
//     ^?
//    this should be never but does not

🙁 Actual behavior

`miauth/${string}/check` & `${string}/timeline` will remain `miauth/${string}/check` & `${string}/timeline`

not coming never would cause problem with Indexed Access Types

function any(): any { throw null; }

type Endpoints = {
  'test/timeline': 0,
  'test2/timeline': 0,
  'other endpoint': 2,
  [ep: `miauth/${string}/check`]: 1, // no error at test2 if comment
}

type TimelineEndpoints = keyof Endpoints & `${string}/timeline`;

const test2: Endpoints[TimelineEndpoints] = any();
const test3: 0 = test2;
// This assignment should not have type errors,
// but because `miauth/${string}/check` & `${string}/timeline` is not never,
// test2 become 0|1 so we cannot assign test2 to test3.

playground

🙂 Expected behavior

`miauth/${string}/check` & `${string}/timeline` should become never and Endpoints[TimelineEndpoints] in snippet above should become 0

Additional information about the issue

Of course, we cannot assign variable typed `miauth/${string}/check` to `${string}/timeline` and vice versa.

function any(): any { throw null; }

const test0: `miauth/${string}/check` = any();
const test1: `${string}/timeline` = test0; // error here, as expected

playground

Real world case: https://github.com/misskey-dev/misskey/pull/14885#discussion_r1832084893

Andarist commented 1 day ago

The problem is not exclusive to template literals - string mappings are also affected:

const test6: Uppercase<string> & `a${string}` = {} as any;
const test7: Uppercase<string> & Lowercase<string> = {} as any;
jcalz commented 1 day ago

Relevant https://github.com/microsoft/TypeScript/pull/40598#issuecomment-693736212

What about get${string} & ${string}Prop? Should that reduce to get${string}Prop? This could get complicated.

I wanna say "no" - matching patterns up to patterns, while possible, I don't think is as intuitive as matching literals to patterns - I see leaving it as get${string} & ${string}Prop as fine. Especially since with something like a${string} & ${string}a, there's no simple template it could merge down to and actually be equivalent, without also extracting specific literal cases for common prefixes and postfixes, like "a" | a${string}a. So yeah, while it's possible, I think it's not as high value - there's probably more clarity in retaining the input patterns, anyway.

RyanCavanaugh commented 15 hours ago

In general we don't have any static guarantees about any two types always producing never when intersected. There is definitely a gray line (literals do, some template literals do, but not all of them), but it is not computable in general to know if two arbitrary types have a nonzero overlap. I don't really want to complicate this logic any further than it already is just to further a problem that won't actually be solved.