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

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

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

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

unional opened this issue Mar 23, 2024 · 11 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
Copy link

@RyanCavanaugh this is the first type I've seen that is not identical to itself via isTypeIdenticalTo:

export type Equals<X, Y> =
    (<T>() => T extends X ? 1 : 2) extends
    (<T>() => T extends Y ? 1 : 2) ? true : false;

type EqualsItself = 
//   ^?  type EqualsItself = false
    Equals<
        `${'abc' & { a: 1 }}`,
        `${'abc' & { a: 1 }}`
    >

Playground link

That wasn't always the case: in v5.0.4 we have the correct type EqualsItself = true.

So maybe there is something unintentional going on here? I'm unsure if it's covered by this existing issue or it's an entirely different issue, but it does seem like there is something broken, as many type-testing libraries rely on this Equals implementation to determine if two arbitrary types are identical (and a copy-pasted type definition should surely be identical in every possible sense).

@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
Copy link

@RyanCavanaugh I agree with your general sentiment, however, that's not quite what I'm trying to get across. Let me simplify and refocus my train of thought by taking out the Equals implementation altogether, as it is somewhat of a red herring:

type ExtendsItself = 
//   ^?  type ExtendsItself = false
    (<T>() => T extends `${'a' & { a: 1 }}` ? 1 : 2) extends 
    (<T>() => T extends `${'a' & { a: 1 }}` ? 1 : 2) ? true : false;

If this should not be considered a regression, then we have the seemingly strange situation above where there exists a type that does not extend itself, similar to some kind of NaN.

Which begs the question: do you consider it a regression, or not? I think the answer to that question will really help to clarify the situation.

@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
Copy link

@RyanCavanaugh yes, I agree with you. But looking at my last comment, can we ignore the concept of Equals for the moment? It is somewhat of a red herring to what I am trying to say, which is that, starting in v5.1, there exist types that no longer extend themselves.

What I want to know is: would you consider that a regression or not?

@HansBrende
Copy link

HansBrende commented Jan 29, 2025

Or, as they say, a picture's worth a thousand words πŸ˜…:

Image

Playground link

@HansBrende
Copy link

Ok, I went ahead and opened a separate issue for the above, since I believe it could be resolved separately from this current issue.

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