microsoft / TypeScript

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

Intersection type in template literal is not reduced to its bare type #57918

Open unional opened 7 months ago

unional commented 7 months ago

πŸ”Ž Search Terms

template literal, intersection type

πŸ•— Version & Regression Information

⏯ Playground Link

https://www.typescriptlang.org/play?#code/C4TwDgpgBAysBOBLAdgcwCrggHgNJQgA9gJkATAZygoRVQD4oBeKfIk8qmpNKAMigoAZhHhQAqgCgAkAH4oAbVwAaCQF0CxUpUXjVuNTPkBvAL6aOO44oAKg5FAAGAEmO5TjtQC4oAV2QA1sgA9gDuDqZGUADkJAC2YAA2AIYkADKIJPDJidEyPtHcdBlZOXnSBUVo5T7IEABuopKSoJBQAEoAjMywtGiYkNhVDJIA9KNQkwB6si1YHQBMPXA8GFhDfaj8UNbJPt2m9GMT07Nzbe0AzMubAzjRyQBGAMbRR+OTUDPn0O0ALDdVndsA8XtFtrt9lBDsdPt8fh0AKyAujAlzGZC+OKPUQed4nL6zVq-ABsKP663RmOxuMcEKgeygB3xcLOUCAA

πŸ’» Code

type StringType<K extends string> = K extends string & infer U
    ? [K, U] extends [U, K]
    ? {} extends { [P in `${K}`]: unknown }
    ? 'templateLiteral'
    : 'stringLiteral'
    : 'string'
    : never

type R1 = StringType<string>
//   ^? type R1 = "string"
type R2 = StringType<string & { a: 1 }>
//   ^? type R2 = "string"

type R3 = StringType<'abc'>
//   ^? type R3 = "stringLiteral"
type R4 = StringType<'abc' & { a: 1 }>
//   ^? type R4 = "templateLiteral" <-- should be "stringLiteral"

type R5 = StringType<`${number}`>
//   ^? type R5 = "templateLiteral"
type R6 = StringType<`${number}` & { a: 1 }>
//   ^? type R6 = "templateLiteral"

πŸ™ Actual behavior

type R = `${'abc' & { a: 1 }}`
// did not reduce => `${'abc' & { a: 1 }}`

πŸ™‚ Expected behavior

type R = `${'abc' & { a: 1 }}`
// should reduce to => `${'abc'}`
// => `abc`

Additional information about the issue

I mentioned this in #54648 after it is closed. It is limiting our ability to write the types that works with string literal and template literal and there is no alternative way to workaround that. I'm suggesting this issue should be fixed and restore the behavior in 5.0.

Here is my original comment:

This behavior is causing a few types in type-plus to fail (e.g. IsTemplateLiteral, IsStringLiteral, Omit, IsNegative, etc) https://github.com/unional/type-plus/issues/429.

In term of soundness, IMO it does make sense that ${string & { a: 1 }} to be reduced to ${string}.

in JS, it would be:

const extendedStr = Object.assign('abc', { a: 1 })
console.log(`${extendedStr}`) // 'abc'

the reasoning being the toString(): string remains unchanged thus the resulting type should be safe to reduce.

jcalz commented 7 months ago
  • This is a crash

No, the tsc process didn't crash.

unional commented 7 months ago

No, the tsc process didn't crash.

Correct. Updated. 🍻

RyanCavanaugh commented 7 months ago

Trying to differentiate template and string literal types seems only possible by relying on weird implementation-detail corner cases and really isn't a great thing to try to offer.

unional commented 7 months ago

Trying to differentiate template and string literal types seems only possible by relying on weird implementation-detail corner cases and really isn't a great thing to try to offer.

It's not just that. This also make it not possible to check for negative number from interaction type.

Speaking of which, it would be great if TS can provide those type utilities. I understand nowadays TS team don't want to add new type utils, but these are types closely related to the language, and like you said, most of them requires a lot of hackaround ways to get them to work.