Skip to content

Add an option to recursively dedent nested template literal strings #12

Closed

Description

@rattrayalex-stripe

Hijacked by @JoshuaKGoldberg on September 4th, 2023: see #12 (comment). Now accepting PRs for a dedent({ recursive: true }) option.


const a = dedent`
hello
  world
`;
const b = dedent`
  foo.
  ${a}
  bar.
`;

results in

foo.
hello
world
bar.

instead of

foo.
hello
  world
bar.

This is unfortunate because it would be nice to compose dedented strings.

If you decide to fix this, any test cases should also check that inlined dedents make sense:

const weird = dedent`
  foo.
  ${things.map(x => dedent`
    hello
      world
  `).join('\n')}
  bar.
`;

Activity

  1. dralletje commented on Sep 19, 2017

    @dralletje

    I needed exactly the same today, so #13 was born.
    It now adds the indention of the line you interpolate on to every line in the interpolated string.

    What would your preferred behaviour for the last example be?
    In my code now, that would result (when you have 3 items in the things array) in this

    foo.
    hello
      world
    hello
      world
    hello
      world
    bar.
    
  2. rattrayalex-stripe commented on Sep 19, 2017

    @rattrayalex-stripe
    Author

    Awesome! Will discuss expected behavior in the PR

  3. ZhouHansen commented on Dec 11, 2017

    @ZhouHansen
  4. bertho-zero commented on May 1, 2018

    @bertho-zero
  5. ZhouHansen commented on May 21, 2018

    @ZhouHansen
  6. bertho-zero commented on May 21, 2018

    @bertho-zero
  7. dralletje commented on May 22, 2018

    @dralletje
  8. ZhouHansen commented on May 22, 2018

    @ZhouHansen
  9. d07RiV commented on Jun 1, 2018

    @d07RiV
  10. dralletje commented on Jun 6, 2018

    @dralletje
  11. rattrayalex commented on Mar 29, 2022

    @rattrayalex
  12. JoshuaKGoldberg commented on May 22, 2023

    @JoshuaKGoldberg
    Collaborator

    Hmm, I can see why you'd want this behavior... but also it's a bit counter-intuitive to how the package is positioned right now IMO. Today, it's used as a "dedent everything in this string" utility - not a "dedent this string, and still allow indents in some contents".

    Perhaps this can become an opt-in? A couple of API starting ideas..

    dedent.recursive`
      ${text}
    `;
    
    dedent({ recursive: true })`
      ${text}
    `;

    Thoughts, everyone?

  13. bertho-zero commented on May 22, 2023

    @bertho-zero
  14. magicmark commented on Sep 4, 2023

    @magicmark

    Perhaps this can become an opt-in? A couple of API starting ideas..
    ...
    Thoughts, everyone?

    @JoshuaKGoldberg thanks for pointing me here! To provide some feedback on this - I use this package a lot (I write a lot of cli tooling in particular that spits out formatted error messages to users) and 10/10 times this is the default behavior i'm looking for. That said, I understand and totally respect it's a question of scope and not wanting to fundamentally change (in a backwards incompatible way) the behaviour of this significantly depended-on library!

    With that in mind, I feel like it's worthy enough of being a top level namespace thing (rather than buried as an option), and so I like what you initially suggested the most:

    dedent.recursive`...`
    

    fwiw in terms of API ideas, here's some prior art for a similar thing in execa: https://github.com/sindresorhus/execa#shared-options - so you'd suggest something like:

    import dedent from 'dedent';
    const ddedent = dedent({ recursive: true });
    console.log(ddedent`...`)
    

    I don't personally love this, the $$ example is neat enough but ddedent or anything else i can think of just looks weird and non obvious. but maybe if you could come up with a catchy alias it could be nice.

    3rd option (which could go alongside as an alias of the first option)

    import { recursive as dedent } from 'dedent';
    console.log(dedent`...`);
    
  15. JoshuaKGoldberg commented on Sep 5, 2023

    @JoshuaKGoldberg
    Collaborator

    The dedent.recursive option is definitely appealing. A similar idea came up with escapeSpecialCharacters too, like dedent.escapeSpecialCharacters. The one issue is that we'd then have to content with a whole bunch of potential options there. Would dedent.escapeSpecialCharacters.recursive need to work too?

    For now, let's add it in as an opt-in option: dedent({ recursive: true }).

  16. added
    type: featureNew enhancement or request
    status: accepting prsPlease, send a pull request to resolve this!
    and removed
    status: in discussionNot yet ready for implementation or a pull request
    on Sep 5, 2023
  17. changed the title [-]Nested dedents does not work as expected[/-] [+]Add an option to recursively dedent nested template literal strings[/+] on Sep 5, 2023
  18. rattrayalex commented on Sep 5, 2023

    @rattrayalex

    Thoughts, everyone?

    Heh, hey @JoshuaKGoldberg ! Good to see you here! Sorry I missed this before.

    FWIW, it's not my expected behavior to have the library dedent most things, except child strings. I consider the current behavior a bug, not a separate feature, though I may be missing something.

    I wouldn't want to have to use an option for a library like this – more specifically, I wouldn't want to have to police that everyone in my codebase always calls the recursive version.

    I could maybe be okay with import dedent from 'dedent/recursive', but really I'd much rather this just be the default behavior (though I'll admit it may require a major version bump).

    For now, personally, I'd probably simply stick with ts-dedent which has this behavior out of the box (as well as TS types) if a recursive option were added.

    Just my 2c!

  19. PaperStrike commented on Aug 4, 2025

    @PaperStrike
    Contributor

    The one issue is that we'd then have to content with a whole bunch of potential options there. Would dedent.escapeSpecialCharacters.recursive need to work too?

    For now, let's add it in as an opt-in option: dedent({ recursive: true }).

    @JoshuaKGoldberg I've implemented this locally and found that the name recursive might be confusing.

    It would be better called alignValues since it only needs to align each interpolated value's indentation to match its starting line's indentation. With this clear functionality in mind, it feels natural to me that it should be enabled by default. Take a look at this branch if you're interested.

    If we ever need to pass the parent options to nested dedent calls, that should be addressed with a separate option.

  20. JoshuaKGoldberg commented on Aug 5, 2025

    @JoshuaKGoldberg
    Collaborator

    alignValues

    👍 makes sense, I like that name. Nice!

    With this clear functionality in mind, it feels natural to me that it should be enabled by default

    tl;dr: this would be a breaking change and we can't do that

    Longer thoughts for transparency: If we were writing a new library this year, I would absolutely agree with that. The proposed behavior makes a lot of sense as a default. But dedent is over a decade old and has roughly 40 million downloads a week. Any small change to behavior to a package with this much widespread adoption comes with a real risk of breaking existing users. The most important thing for the project is to keep things running smoothly for existing users.

    Given that this behavior is not technically a bug, just a preference (albeit one we all differ from the default behavior on), it wouldn't be appropriate to release it in a minor version. That'd have to be a breaking change for a new major version. Which we totally could do if it was warranted! I don't think this one behavior preference warrants a new major, but if enough requests come in, that could be something to discuss separately.

  21. added a commit that references this issue on Sep 3, 2025
    304d0fc
  22. github-actions commented on Sep 3, 2025

    @github-actions

    🎉 This is included in version v1.7.0 🎉

    The release is available on:

    Cheers! 📦🚀

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

    status: accepting prsPlease, send a pull request to resolve this!type: featureNew enhancement or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions