-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Bug: Circular references not allowed for template literal types #44792
Comments
Thereβs a whole machinery for lazily normalizing tuples and representing their non-normalized forms. It seems quite likely to me that the analogous machinery doesnβt exist for template literal types and would be a lot of complexity to set up, but Iβm not sureβpinging @ahejlsberg for a quick judgment of how impossible this sounds π |
Please correct me if I'm wrong, but I believe this would go a long way to solving the issue of exploding template literal types when using unions, no? i.e. stop producing the error: "Expression produces a union type that is too complex to represent" when trying to do something like below, or in this Playground. type Numeric = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
type Alphabetic =
| 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h' | 'i' | 'j' | 'k' | 'l' | 'm'
| 'n' | 'o' | 'p' | 'q' | 'r' | 's' | 't' | 'u' | 'v' | 'w' | 'x' | 'y' | 'z'
type Alphanumeric = Alphabetic | Numeric
type Repeat<
Char extends string,
Count extends number,
Joined extends string = ``,
Acc extends 0[] = []
> = Acc['length'] extends Count ? Joined : Repeat<Char, Count, `${Joined}${Char}`, [0,...Acc]>
// Error: Expression produces a union type that is too complex to represent. (2590)
type UUIDV4 = `${Repeat<Alphanumeric, 8>}-${Repeat<Alphanumeric, 4>}-${Repeat<Alphanumeric, 4>}-${Repeat<Alphanumeric, 4>}-${Repeat<Alphanumeric, 12>}` Of course this works when used for a smaller number of repeats (which is as designed β I believe the limit is 100,000 types in a single union, as per this comment on another issue): // type Hex = "aaa" | "aab" | "aac" | "aad" | "aae" | "aaf" | "aag" | "aah" | "aai"
// | "aaj" | "aak" | "aal" | "aam" | "aan" | "aao" | "aap" | "aaq" | "aar" | "aas" | "aat"
// | "aau" | "aav" | "aaw" | ... 46632 more ... | "999"
type Hex = Repeat<Alphanumeric, 3> But if instead of producing a union type and exploding, what if template literal types were lazily evaluated and produced something like: // type UUIDV4 = `${Alphanumeric} ... 6 more ... ${Alphanumeric}-${Alphanumeric}
// ... 2 more ... ${Alphanumeric}-${Alphanumeric} ... 2 more ... ${Alphanumeric}-${Alphanumeric}
// ... 2 more ... ${Alphanumeric}-${Alphanumeric} ... 10 more ... ${Alphanumeric}`
type UUIDV4 = `${Repeat<Alphanumeric, 8>}-${Repeat<Alphanumeric, 4>}-${Repeat<Alphanumeric, 4>}-${Repeat<Alphanumeric, 4>}-${Repeat<Alphanumeric, 12>}` @andrewbranch, is this pretty much the same thing you had in mind when you said it'd probably be too complex to add the necessary infrastructure to support? |
Yes π |
@andrewbranch Thanks. I'm [obviously] not an expert on the typescript compiler, but would you mind sharing why you think it might be a complex change? Is it because of technical debt; or an architectural issue or something? e.g. maybe it'd be too hard to reuse code from the existing recursive feature(s)? I'm mostly wondering out of professional curiosity more than anything! π |
Iβm honestly not enough of an expert on how we deal with deferred recursive types to be 100% confident about how difficult this would be and what the biggest challenges would be, but Iβm familiar enough to have an intuition that it would be complicated. Even if I were wrong about that, I thinkΒ weβd want to see compelling use cases for why we should add this kind of complexity. Template literal types were created in large part to support mapped type |
Thanks, that really helps to understand the thought process behind adding template literal types ππΌ I'm curious though, exactly which of the characteristics/traits of that relationship made it a compelling use case to the team? I mean it's obviously, without question, a very compelling reason π but phrased another way, I guess my question is what goes into making the teams' decision-making process and/or how does the team actually measure/quantify how compelling one feature is/might be over another? π |
@andrewbranch For use cases, I can think of a couple off the top of my head:
interface ZTy {
Number: number;
String: string;
}
interface OT<T> {
Array: Array<T>;
}
type ZType = keyof ZTy;
type OType = keyof OTy<any>;
type CType = ZType | `${OType}[${CType}]`;
type MapTy<C extends CType> = C extends ZType ? ZTy[C] :
C extends `${infer OT}[${infer PT}]` ? OT extends OType ? PT extends CType ? OTy<MapTy<PT>>[OT]
: never : never : never;
type StrArr = MapTy<"Array[String]">;
type NestedNumArr = MapTy<"Array[Array[Number]]">; Which would be pretty cool. Specifically, it would allow you to better emulate partially-applied types. |
Neither tuple types nor template literal types permit true recursion. We support rest elements in tuple types (the type StrNumPairs = [] | [string, number, ...StrNumPairs]; // Error, circular It certainly would be nice to allow that (in tuples as well as template literals), but it would add significant complexity. For example, we currently normalize tuple and template literal types, allowing us to share representation of structurally equivalent types. We'd only be able to partially do that for potentially recursive types. Also, code that processes tuple and template literal types, such as relationship checking, type inference, and type instantiation, would need lazy partial materialization and infinite recursion guards and limits. It's all possible, but it is complex and I'm not sure the slight added expressiveness merits that complexity. |
@ahejlsberg Yeah, I'm always running into issues w/ recursion support. That said, I come from a dependent-types background so I might be pushing the type system too hard. I'd still +1 the idea, despite the complexities, but industry may feel differently. |
@ahejlsberg, thanks for the explanation; I appreciate it π Totally understand that code is cost. Every line you don't write is an improvement π Would the code you'd need for lazy partial materialization and the infinite recursion guards not be generalizable enough to reuse elsewhere? Without any knowledge of the codebase myself, could you not refactor it to be used by other recursive types (including future ones that may not yet exist)? Could that not then theoretically reduce the overall complexity? π€ Just spitballing here; you and the team are obviously the experts π (but you're also cursed with the expertise π) |
A simple use case (for which I have several variations on the theme) in a codebase I'm working on involves the following: const enum Moods
{
Happy = "happy",
Sad = "sad",
Worried = "worried",
//... some 50 more...
Joyous = "joyous",
}
// Type alias 'MoodList' circularly references itself.ts(2456)
type MoodList = `${Moods}` | `${Moods}, ${MoodList}`;
interface SomeObject
{
/** Comma separated list of well-known Mood values. */
moods: MoodList;
} At present I make do with |
In my case, I'm giving HTTP headers specific types. For example, for allow header of HTTP |
Many apis generate and consume comma delimited lists of known terms. type Term = "term1" | "term2" | "term3";
const terms: ??? = "term1, term3";
function getTerms(): ??? {}
function takeTerms(terms: ???) {} It would be real handy for takeTerms(getTerms()); to type check. |
Maybe explicitly marking something as // Simple, explicit keyword
type MyRecursivePath = `typescript.is${many '.super'}.cool`;
// Regex style (I personally don't like it but hey π€·ββοΈ)
type MyRecursivePath = `typescript.is${'.super'?}.cool`; // 0 or 1 repetition
type MyRecursivePath = `typescript.is${'.super'+}.cool`; // 1 or more repetition
type MyRecursivePath = `typescript.is${'.super'*}.cool`; // No idea how many repetition
// Array style
type MyRecursivePath = `typescript.is${'.super'[]}.cool`; // 0 or 1 repetition So the definition is explicit and the description does not resolve anything, it spits out the declaration. Obviously I have very little knowledge of the compiler, but if the biggest burden is the resolving the circular reference, then explicit declaration would solve that part. On a side note I would like to say that most people interested in this feature are probably library maintainers, so I would not expect a lot of feedbacks, but it would impact a lot of users. |
Bug Report
π Search Terms
Related issues: #43335.
π Version & Regression Information
β― Playground Link
Playground link with relevant code
π» Code
π Actual behavior
You get the error:
π Expected behavior
This should type check like with the tuple example - it's the same principle, just with template literals.
The text was updated successfully, but these errors were encountered: