Skip to content

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

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

Open
unional opened this issue Mar 23, 2024 · 12 comments
Open

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

unional opened this issue Mar 23, 2024 · 12 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@unional
Copy link
Contributor

unional commented Mar 23, 2024

πŸ”Ž 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) unional/type-plus#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

This comment was marked as resolved.

@unional
Copy link
Contributor Author

unional commented Mar 24, 2024

No, the tsc process didn't crash.

Correct. Updated. 🍻

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature labels Mar 25, 2024
@RyanCavanaugh
Copy link
Member

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
Copy link
Contributor Author

unional commented Mar 25, 2024

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.

@HansBrende

This comment has been minimized.

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Jan 29, 2025

The Equals type there is (intentionally) reliant on internal compiler implementation details and shouldn't be used unless you're actively trying to break something. Its use in type testing libraries is bad and I would discourage its use.

@HansBrende

This comment has been minimized.

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Jan 29, 2025

Internal implementation details are subject to change at any time. Equals may evaluate to true but there are no guarantees on this -- it's dependent on whether or not we choose to intern a given type, which is a performance trade-off that may be situationally-dependent.

The origin of this type is that someone went looking for places where type identity might be unintentionally exposed, was successful, and then introduced this into the wild before we noticed and could un-expose it without breaking a ton of people. TS doesn't have any defined semantics of "identical" types, and attempts to reverse-engineer one are not going to be successful as a result.

@HansBrende

This comment has been minimized.

@HansBrende

This comment has been minimized.

@HansBrende

This comment has been minimized.

@HansBrende
Copy link
Contributor

HansBrende commented May 7, 2025

Update on my above comments: I fixed that slightly tangential issue in #61113, which was merged a few days ago (incidentally, also fixing that particular problem with the Equals hack).

So to re-focus on the issue at hand: personally, I'm ambivalent whether tagged strings should have their tags removed in a template string.

However, the big issue I do see here is that there is no way to remove tags from a literal if you don't know the exact type of the tag(s) in advance.

I.e., it is utterly impossible to write an Untag<T> type. For any function you come up with, I can give you a literal that can't be untagged. Here's a good test-case:

type Evil = 1234 & {a: 0 | 1} & {a: 1 | 2} & {toString: Number['toString']}

type YourFailedAttempt = Untag<Evil> // Expected output: 1234

This means that libraries that do conditional string manipulation, etc. will necessarily fail if the user passes in any tagged primitive. It would be really nice to have some way of stripping arbitrary tags from an arbitrary primitive literal.

The very least intrusive fix, I believe, would be to allow the following behavior to work:

type TheTag = Evil extends number & infer TAG ? TAG : unknown

Critically, TheTag would need to be everything in Evil except the numeric part. Then we could very simply do: OriginalLiteralType = Evil extends TheTag & infer N ? N : never to retrieve our original literal. (This would have to work for string, symbol, and bigint as well, of course.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

4 participants