Repository navigation
5.1.3 new "feature"? Branded types are preserved in const template literalΒ #54648
Description
Activity
RyanCavanaugh commented
on Jun 14, 2023 MemberMore actionsI think this was intentional? (I at least explicitly added it and tests for it, though in the course of fixing other stuff.)
Though, I'm not sure how this should interact with as const... I guess it's a noop and that's normal?
The relevant PR is #52836, note that the tests contain effectively exactly what the original post contains, a branded type inside a template.
conorbrandon commented
on Jun 15, 2023 AuthorMore actionsAh, template literals, that's the word I was looking for (instead of string interpolation). Had a brain fart, my apologies.
- 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 - addedWorking as IntendedThe behavior described is the intended behavior; this is not a bugThe behavior described is the intended behavior; this is not a bug
on Jun 19, 2023 typescript-bot commented
on Jun 22, 2023 ContributorMore actionsThis issue has been marked as "Working as Intended" and has seen no recent activity. It has been automatically closed for house-keeping purposes.
- addedBugA bug in TypeScriptA bug in TypeScriptand removedWorking as IntendedThe behavior described is the intended behavior; this is not a bugThe behavior described is the intended behavior; this is not a bug
on Nov 15, 2023 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,S2would resolve tostring. Currently,S2resolves to`xyz${"a" & { tag: any }`. Neither of those are right,S2should just resolve to"xyza".2 remaining items
There's also #54188 which I really don't like. We decided to allow
string & {} | "a" | "b"to not reduce to juststringbecause 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.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.
Reacted by Conor BrandonJake 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.
Just to link it (since it's in the middle of a thread), #52345 (comment) is the perf case I had been looking into.
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.Funny, I had the opposite takeaway from the meeting given the potential perf benefit π
My chief performance concern was the overhead of descending into template literal types in
getGenericObjectFlagsand adding theobjectFlagsproperty 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.- addedWorking as IntendedThe behavior described is the intended behavior; this is not a bugThe behavior described is the intended behavior; this is not a bugand removedBugA bug in TypeScriptA bug in TypeScript
on Nov 16, 2023 This behavior is causing a few types in
type-plusto 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(): stringremains unchanged thus the resulting type should be safe to reduce.conorbrandon commented
on Mar 15, 2024 AuthorMore actionsHoma Wong (@unional) That is a valid point, indeed.
conorbrandon commented
on Mar 15, 2024 AuthorMore actionsIt 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
KeyWideactually includes aUserID,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.
Reacted by Homa Wong- locked as resolved and limited conversation to collaborators
on Oct 22, 2025
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
π 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 π