Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lazily evaluated template literal types #43335

Open
5 tasks done
fabiospampinato opened this issue Mar 22, 2021 · 12 comments
Open
5 tasks done

Lazily evaluated template literal types #43335

fabiospampinato opened this issue Mar 22, 2021 · 12 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@fabiospampinato
Copy link

fabiospampinato commented Mar 22, 2021

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.

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

@fabiospampinato
Copy link
Author

fabiospampinato commented Mar 22, 2021

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.

@RyanCavanaugh RyanCavanaugh added Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript labels Mar 22, 2021
@Fleker
Copy link

Fleker commented Jun 23, 2021

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.

@footurist
Copy link

footurist commented Nov 20, 2022

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

@alex-statsig
Copy link

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

@AtomicGamer9523
Copy link

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

@MichalMarsalek
Copy link

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?

@anonhostpi
Copy link

anonhostpi commented Aug 22, 2024

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

@HansBrende
Copy link

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}`

@ArtemAvramenko
Copy link

ArtemAvramenko commented Dec 18, 2024

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}`;

@danielbayley
Copy link

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

or…

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

@shaedrich
Copy link

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
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests