Skip to content

5.1.3 new "feature"? Branded types are preserved in const template literalΒ #54648

Description

Bug Report

πŸ”Ž Search Terms

branded, interpolation, 5.1.3

πŸ•— Version & Regression Information

This changed between versions 5.0.4 and 5.1.3

⏯ Playground Link

Playground link with relevant code

πŸ’» Code

type S = `id_${string & {_brand: "ID"}}`;
//   ^? type S = `id_${string & { _brand: "ID"; }}`
declare const id: string & {_brand: "ID"};
const s = `id_${id}` as const;
//    ^? const s: `id_${string & { _brand: "ID"; }}`

πŸ™ Actual behavior

The intersected brand is preserved during string interpolation for variables and types!

πŸ™‚ Expected behavior

Pre 5.1.3, the intersected brand always got removed during string interpolation for variables and types.

This is part bug report and question. My question being in future versions, will this behavior be preserved? I wrote a clunky generic function and type to do this exact thing because I found it valuable, but I'd much rather rely on native behavior if this is going to stick around πŸ‘€

Activity

  1. RyanCavanaugh commented on Jun 14, 2023

    @RyanCavanaugh
    Member

    I think this was (unintentional?) fallout from #48034 / #52345

  2. jakebailey commented on Jun 15, 2023

    @jakebailey
    Member

    I think this was intentional? (I at least explicitly added it and tests for it, though in the course of fixing other stuff.)

    Also Wesley Wigham (@weswigham)

  3. jakebailey commented on Jun 15, 2023

    @jakebailey
    Member

    Though, I'm not sure how this should interact with as const... I guess it's a noop and that's normal?

  4. jakebailey commented on Jun 15, 2023

    @jakebailey
    Member

    The relevant PR is #52836, note that the tests contain effectively exactly what the original post contains, a branded type inside a template.

  5. conorbrandon commented on Jun 15, 2023

    @conorbrandon
    Author

    Ah, template literals, that's the word I was looking for (instead of string interpolation). Had a brain fart, my apologies.

  6. changed the title [-]5.1.3 new "feature"? Branded types are preserved in `const` string interpolation[/-] [+]5.1.3 new "feature"? Branded types are preserved in `const` template literal[/+] on Jun 15, 2023
  7. typescript-bot commented on Jun 22, 2023

    @typescript-bot
    Contributor

    This issue has been marked as "Working as Intended" and has seen no recent activity. It has been automatically closed for house-keeping purposes.

  8. added
    BugA bug in TypeScript
    and removed
    Working as IntendedThe behavior described is the intended behavior; this is not a bug
    on Nov 15, 2023
  9. ahejlsberg commented on Nov 15, 2023

    @ahejlsberg
    Member

    I'm reopening this one. The original issue, #48034, was that we're not "untagging" literal types when they occur in template literal placeholders. In other words, the expectation was that

    type S1 = `xyz${"a"}`;
    type S2 = `xyz${"a" & { tag: any }}`;

    both resolve to "xyza". Originally, S2 would resolve to string. Currently, S2 resolves to `xyz${"a" & { tag: any }`. Neither of those are right, S2 should just resolve to "xyza".

  10. 2 remaining items

  11. ahejlsberg commented on Nov 15, 2023

    @ahejlsberg
    Member

    There's also #54188 which I really don't like. We decided to allow string & {} | "a" | "b" to not reduce to just string because the pattern was in use in some popular DT packages. But it's not at all clear we want to broaden this support to template literals--and even if we did, I would think it's the template literal itself that should be intersected with {}, not one of the placeholders.

  12. jakebailey commented on Nov 15, 2023

    @jakebailey
    Member

    I guess we'll talk about it in the design meeting, but I can't help but feel a little sad given this issue was about the construct being useful, and I found the branded case to be a valuable idea: https://github.com/microsoft/TypeScript/blob/e170bc59d4ba0d335ce86f66296bec71c5018317/tests/cases/compiler/templateLiteralIntersection2.ts

    I'll have to resurrect the perf test I had that motivated my follow-up.

  13. ahejlsberg commented on Nov 15, 2023

    @ahejlsberg
    Member

    Jake Bailey (@jakebailey) I saw that test, but I'm not sure I've seen an actual user request for the feature. Other than just stripping tags. I'm just not convinced anything beyond that is worth the complexity.

  14. jakebailey commented on Nov 15, 2023

    @jakebailey
    Member

    Just to link it (since it's in the middle of a thread), #52345 (comment) is the perf case I had been looking into.

  15. ahejlsberg commented on Nov 16, 2023

    @ahejlsberg
    Member

    My inclination was otherwise, but since the consensus in the design meeting trended towards preserving tagged literals in placeholders, I'm going to go along. We still need a few fixes however, such as #53427 and not descending into template literals in getGenericObjectFlags. Plus the issue I mentioned here. I will put up a PR to that effect.

  16. jakebailey commented on Nov 16, 2023

    @jakebailey
    Member

    Funny, I had the opposite takeaway from the meeting given the potential perf benefit πŸ˜…

  17. ahejlsberg commented on Nov 16, 2023

    @ahejlsberg
    Member

    My chief performance concern was the overhead of descending into template literal types in getGenericObjectFlags and adding the objectFlags property to it. I'm backing that out in my PR as it isn't necessary. Beyond that, there is of course some overhead associated with preserving tagged literal types in placeholders, but you only incur that overhead if you use tagged literal types. It seemed like most folks liked the preservation and I'm willing to go with it.

  18. added
    Working as IntendedThe behavior described is the intended behavior; this is not a bug
    and removed
    BugA bug in TypeScript
    on Nov 16, 2023
  19. unional commented on Mar 12, 2024

    @unional
    Contributor

    This behavior is causing a few types in type-plus to fail (e.g. IsTemplateLiteral, IsStringLiteral, Omit, IsNegative, etc) cyberuni/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.

  20. conorbrandon commented on Mar 15, 2024

    @conorbrandon
    Author

    Homa Wong (@unional) That is a valid point, indeed.

  21. conorbrandon commented on Mar 15, 2024

    @conorbrandon
    Author

    It didn’t come up in the original discussion, but I might as well add it now. The specific use case for this relates to DynamoDB keys. A common pattern for such keys is, for example, USER#${UserID}. In these template literals, it has proven valuable to have a stricter type that includes the brand to avoid mistakes, especially during refactors.

    i.e., this:

    type UserID = string & { [BRAND]: "UserID" };
    type Key = `USER#${UserID}`;

    vs. this:

    type KeyWide = `USER#${string}`;

    Of course, there are workarounds, such as a helper function to enforce a KeyWide actually includes a UserID,

    const formatUserKey = (userID: UserID): KeyWide => `USER#${userID}`;

    but it's convenient to be able to write them literally as well, and have stronger assurances:

    const foo = "foo";
    const key: Key = `USER#${foo}`; // oops!

    So I would like to see this stick around (as I mentioned, it has proven valuable), however, I do understand that "it's convenient" is not the strongest of arguments, especially if this is causing issues elsewhere.

  22. unional commented on Mar 17, 2024

    @unional
    Contributor

    Connecting related issues:

    #49839
    #54648
    #57776
    #57807

  23. 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

Labels

Working as IntendedThe behavior described is the intended behavior; this is not a bug

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions