Skip to content

Intrinsic string mapping type with type parameters is incorrectly resolved in conditional types #55847

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

Closed
sankantsu opened this issue Sep 24, 2023 · 4 comments · Fixed by #55856
Labels
Bug A bug in TypeScript Help Wanted You can do this
Milestone

Comments

@sankantsu
Copy link

sankantsu commented Sep 24, 2023

Bug Report

🔎 Search Terms

lowercase, uppercase, intrinsic, conditional type

🕗 Version & Regression Information

  • This changed between versions 5.0.4 and 5.1.0-beta.
  • 5.2.2 and nightly still have the same problem.
  • I also reviewed the FAQ for entries about lowercase, uppercase, intrinsic and conditional types.

⏯ Playground Link

https://www.typescriptlang.org/play?ts=5.1.6#code/C4TwDgpgBAggPAZQHxQLxQDIHsDuEBOAxgIYDOEiAZKcPgJYB2A5ihAB7AQMAmpUARADMsWflAD8UAIxQAXFAAMAKAD0KqFAB64paEhQAQohTpseImQoJqtRiyjtOPPgANBAEgDeNeswC+LhLScoqq6lo6SgA2EMBQbFLy8EIi-CbBag74+Fj40bHxAEzyRimi6TKZBDn4QA

💻 Code

type A<S> = Lowercase<S&string> extends "foo" ? 1 : 0
//  ^?  type A<S> = 0
type B<S> = Lowercase<S&string> extends `f${string}` ? 1 : 0
//  ^? type B<S> = 0

let x1: A<"foo"> = 1 // error
let x2: B<"foo"> = 1 // error

🙁 Actual behavior

A<"foo"> = 0 and B<"foo"> = 0.
Actually, these conditional branches are resolved at the time of type alias definition.
Then, any instantiation of A and B is 0

🙂 Expected behavior

Both A<"foo"> and B<"foo"> should be 1.
Conditional types in A and B should not be resolved during type alias definition, but should be deferred until an instantiation.

Additional information about the issue

Related

#52102 is similar.
This issue is about incorrect "simplification" involving intrinsic string mapping types.

More info

I did some investigation in codebase.
It looks like that the cause of the problem is handling of string mapping type in getGenericObjectFlags or isPatternLiteralPlaceholderType which was introduced in #52836 .

https://github.dev/microsoft/TypeScript/blob/97147915ab667a52e31ac743843786cbc9049559/src/compiler/checker.ts#L17967

@MartinJohns
Copy link
Contributor

Workaround:

type A<S extends string> = Lowercase<S> extends "foo" ? 1 : 0
type B<S extends string> = Lowercase<S> extends `f${string}` ? 1 : 0

Not sure why you do the intersection with string. That seems rather odd.

@sankantsu
Copy link
Author

Yes, I admit that its more natural to use type constraints in above example.

In fact, I found this issue while using Lowercase combined with mapped type.
But it turned out that mapped type was not the cause of problem and I decided to use more simple example in above description.

Here is a more motivating example.

type CamelCase<S extends string> =
  Lowercase<S> extends `${infer S1}_${infer S2}` ? `${S1}${Capitalize<S2>}` : Lowercase<S>

type RenameKeys<T> = {
  [K in keyof T as CamelCase<K&string>]: T[K]
}

type A = CamelCase<"foo_bar">
//  ^?  type A = "fooBar"
type B = RenameKeys<{foo_bar: number}>
//  ^?  type B = { foo_bar: number }
// should be B = { fooBar: number }

Playground link

I think it is natural to use K&string in this example because we want to consider only string keys.

@MartinJohns
Copy link
Contributor

I think it is natural to use K&string in this example because we want to consider only string keys.

Why not keyof T & string or Extract<keyof T, string>?

@fatcerberus
Copy link

@MartinJohns I think that makes the mapped type not homomorphic

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
4 participants