Skip to content

Lazily evaluated template literal types #43335

Description

Suggestion

Template literal types seem to be resolved immediately, which makes the combinatorics blow up and the whole thing much less useful than it would seem to be at first.

For example I can't even represent a type equals to 5 consecutive digits with the current system without getting a "too complex" error:

type Digit = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
type UUID = `${Digit}${Digit}${Digit}${Digit}${Digit}`;

The current system may be more composable than regexes, but if it's not even able to represent something like /\d{5}/ I'd argue that's not a replacement for it at all.

The suggestion is that there's no need to resolve all the possible combinations at all, but a divide and conquer approach should be implemented where each little piece of the template literal does it's job individually and when all little pieces match than the whole thing matches.

🔍 Search Terms

  • template literal
  • lazy

✅ Viability Checklist

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

⭐ Suggestion

Evaluate template literal types lazily to not make the combinatorics blow up. Meaning that each little piece of the template literal should be its own little function that performs some type matching internally, so there's no need to resolve all the possible combinations at all.

📃 Motivating Example

I can't even represent the equivalent of /\d{5}/ currently.

type Digit = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;
type UUID = `${Digit}${Digit}${Digit}${Digit}${Digit}`;

💻 Use Cases

For example creating a type for UUID strings, which are a fairly common thing.

Activity

  1. MartinJohns commented on Mar 22, 2021

    @MartinJohns
    Contributor

    Related: #41160

    The current system may be more composable than regexes, but if it's not even able to represent something like /\d{5}/ I'd argue that's not a replacement for it at all.

    It's not meant as a replacement. It's also not meant for writing DSL, yet people try to abuse it for that purpose.

  2. fabiospampinato commented on Mar 22, 2021

    @fabiospampinato
    Author

    It's not meant as a replacement.

    I might be misremembering but I think I read a comment from one of the maintainers saying that template literal types got implemented because it's more clear how to compose them, while the same was not true for regex types, which are not currently supported.

    That doesn't mean that the current implementation is meant to be a replacement for regexes, even though there's some overlap of course.

    I just wanted to touch on how regex types would be able to represent vastly more complicated spaces of strings than what template literal types can currently do, especially given the way TypeScript currently implements them.

  3. Fleker commented on Jun 23, 2021

    @Fleker

    I've run into this issue with a Typescript-based game I've been building. Users can collect rewards that are serialized in a specific format and can be composed with hyphens.

    'item-001'
    'item-002-sunny'
    'item-002-var0'
    'item-003-sunny-var0'

    I have a custom class to perform deserialization and serialization, although I do have a bunch of hardcoded strings scattered around. Originally I just used a string type, but it was too easy to make typos.

    'itm-001'
    

    And rewarding this broken item to a player caused other issues in the game, resulting in a bit of manual work in player accounts.

    Eventually I created a shell script that would scan my item database and create an explicit type, although it's limited because it's difficult to represent the entire composability.

    export type TypeItem = 'item-001' | 'item-002' | 'item-003' | ...
    const Item1 = 'item-001'
    const Item2 = 'item-002'
    const Item3 = 'item-003'

    While this is an improvement, I do need to do quite a bit of casting for my composing unless I really wanted to update my shell script.

    const prize = '${Item1}-sunny' as TypeItem

    Which still makes this prone to potential typos with the added compositions.

    My game would benefit a lot from being able to use template literal types, as I have a use-case which makes a lot of sense. However, trying to define the composition in full fails with the error Expression produces a union type that is too complex to represent.

    type N = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
    type PartExt = '-sunny' | '-rainy' | '-snowy' | '' // ...
    type PartVar = '-var1' | '-var2' | '-var3' | ''
    type PartSpecial = '-special' | ''
    export type TypeItem = `item-${N}${N}${N}${PartExt}${PartVar}${PartSpecial}`

    I can represent a slightly smaller type, with a union of four types:

    export type TypeItem = `item-${N}${N}${N}${PartExt}`

    Which, when hovered over, suggests there's about 58,000 entries. Adding any other Part causes an error, but I can compose the smaller types together.

    export type TypeItem = `item-${N}${N}${N}${PartVar}${PartSpecial}`

    Which, suggests there's about 30,000 entries.

    By expanding the number of strings in PartExt, my union type item-${N}${N}${N}${PartExt} will create compiler errors at 100,000 possible entries.

    Perhaps trying to implement this serialized representation as a Template Literal is not the right approach, particularly as I already have a class for managing these values. At the same time, I'm not sure that my union type is too complicated that the Typescript compiler cannot process 100K items.

  4. footurist commented on Nov 20, 2022

    @footurist

    Anything new on this? Just encountered the "need" for this while creating a template literal type for a string joined with colons. I know one could simply just change the implementation to taking an array, but sometimes it appears to be nice to have to be able to type an infinite sequence like in Haskell for example and have it be evaluated lazily :

    type Tag = "html" | "body"
    type ID = `#${ "app" | "item" }`

    type PseudoClass = "hover" | "before"

    type WithAfter = `${ Tag | ID }..:${ PseudoClass }..:after`

    ..${ pattern }.. => allow for indefinite repetition and lazy evaluation

  5. alex-statsig commented on Feb 22, 2023

    @alex-statsig

    Another use case (similar to one mentioned in a linked issue) is typing IDs more strongly. Our IDs are stored as strings, and we don't want to wrap them in an unnecessarily heavy class or object (ex. type ID = { id: string; __type: 'id' }). However, we do want to ensure that things like loadObject() take in an ID obtained for somewhere else, ensuring loadObject('') is not valid.

    To do this, we've defined some types:

    export type SUIDString = '%do not hardcode: SUID%';
    export type UUIDString = `${string}-${string}-${string}-${string}-${string}`;
    

    The UUID one (while not a perfect match for UUIDs) works rather well, as any hardcoded "correctish" string will pass, and our helpers can assert they return it. The SUIDString has a more generic format (any [a-zA-Z0-9] of length 10-15) that can't be expressed currently. This means that any literals seem to fail it (preventing object.getID() === 'a real id' from compiling).

  6. matveitolochko commented on Jun 25, 2024

    @matveitolochko

    +1
    I encountered this issue when trying to implement hexadecimal color codes.
    I could potentially implement this, given that this seems to be quite useful and wanted.

  7. MichalMarsalek commented on Jul 5, 2024

    @MichalMarsalek
    Contributor

    I'd love to see this. Can this issue be put into backlog so that we can implement it or closed to indicate it won't get supported?

  8. anonhostpi commented on Aug 22, 2024

    @anonhostpi

    Even if handling of literal types can't be modified, an alternative solution should be to have string subtypes for common string patterns.

    The most significant example would be hexadecimal strings. This would add string validation for common formats like RGBA color data, byte/memory representation, or UUIDs.

    There are several issues that have been opened asking for this kind of feature where the requestor's use case was specifically for hexadecimal validation

  9. HansBrende commented on Dec 16, 2024

    @HansBrende
    Contributor

    This would solve (or greatly ease) nearly every use-case for fixed-length string validation, so +1 from me. I can't describe how many times I have yearned for the following type signature to work:

    type RGB = `#${Hex}${Hex}${Hex}${Hex}${Hex}${Hex}`
  10. ArtemAvramenko commented on Dec 18, 2024

    @ArtemAvramenko

    It would be nice to support specifying ranges instead of completely listing all values as syntactic sugar.

    For example:

        type HexDigit = '0'..'9' | 'A'..'F' | 'a'..'f';

    For simplicity, this can be restricted to single characters only, excluding surrogate pairs as well.

    I'm not sure, but maybe this could also optimize storing such a type in compiler memory without generating undecillions of all possible combinations ahead of time. For example, this could apply to these types:

    type HexDigitQuad = `${HexDigit}${HexDigit}${HexDigit}${HexDigit}`;
    type GuidString = `${HexDigitQuad}${HexDigitQuad}-${HexDigitQuad}-${HexDigitQuad}-${HexDigitQuad}-${HexDigitQuad}${HexDigitQuad}${HexDigitQuad}`;
  11. danielbayley commented on Dec 18, 2024

    @danielbayley
    type HexDigit = '0'..'9' | 'A'..'F' | 'a'..'f';
    

    or…

    type HexDigit = '[0-9]' | '[A-F]' | '[a-f]'; // or '[A-f]'
  12. shaedrich commented on Dec 18, 2024

    @shaedrich

    It would be nice to support specifying ranges instead of completely listing all values as syntactic sugar.

    see #54925 (and #15480) for reference

    or…

    type HexDigit = '[0-9]' | '[A-F]' | '[a-f]'; // or '[A-f]'

    see #41160 (and #6579) for reference

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

    Awaiting More FeedbackThis means we'd like to hear from more people who would be helped by this featureSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions