Skip to content

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

Description

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

Activity

  1. MartinJohns commented on Sep 24, 2023

    @MartinJohns
    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.

  2. sankantsu commented on Sep 24, 2023

    @sankantsu
    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.

  3. MartinJohns commented on Sep 24, 2023

    @MartinJohns
    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>?

  4. fatcerberus commented on Sep 24, 2023

    @fatcerberus

    Martin Johns (@MartinJohns) I think that makes the mapped type not homomorphic

  5. locked as resolved and limited conversation to collaborators on Oct 22, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    BugA bug in TypeScriptHelp WantedYou can do this

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions