TypeScript: Circular reference error when defining Record type
TypeScript Version: 4.0.2 (and 4.1.0-dev.20201015)
Search Terms: circularly, Record
Code
type T1 = { [key: string]: T1 }
type T2 = Record<string, T2>
Expected behavior: No compilation errors
Actual behavior:
While type T1 compiles properly, type T2 reports Type alias 'T2' circularly references itself. even though they are inherently the same type. This was actually found by autofixing a new @typescript-eslint rule consistent-indexed-object-style
Playground Link: https://www.typescriptlang.org/play?ts=4.0.2#code/C4TwDgpgBAKgjFAvFA3lA2gawiAXFAZ2ACcBLAOwHMBdfeKAXwFgAoUSWAJiSgCUIAxgHtiAEwA8RMlQA0XAHxA
Related Issues: None found
About this issue
- Original URL
- State: closed
- Created 4 years ago
- Reactions: 22
- Comments: 19 (2 by maintainers)
@RyanCavanaugh not sure what’s under the hood of
Record, but it does feel a bit inconsistent that some generics seem to work fine with recursions while others don’t:Certain circularities are allowed (e.g. T1) but other circularities aren’t, e.g.
Generic instantiation is deferred, so at the point TS analyzes
T2’s declaration, it can’t tell if it’s in theRecordcase (would be allowed) or theIdentitycase (would not be allowed). It’s only later in the process that we could tell that this is actually OK, but if it wasn’t OK, it’s “too late” to go back and start complaining about it.@jedwards1211 good point, also what you’ve described is a workaround for this very issue!
playground
The thing that’s frustrating about this is if we define
Because of the
=the natural expectation is that both sides are truly equivalent and interchangeable. But this equivalence gets violated when we takeAnd then try to substitute the RHS with what should be equivalent:
So
MyRecord<V>isn’t actually equivalent to{[key: string]: V}in all cases. Are there other cases I don’t know about where the sides of a type alias aren’t equivalent?Why is it so painful to write recursive programs with TypeScript?
What’s the fix without a bunch of goofy business?
This seems work:
{ [key: string]: T } may not be very useful since
{ 'foo': 'bar' }is aRecord<string, string>but not a{ [key: string]: T }BTW, the issue I mentioned with typescript-eslint was fixed (https://github.com/typescript-eslint/typescript-eslint/issues/2687)
@RyanCavanaugh The behaviour seems a bit inconsistent to me (or at least not very transparent)
In the following example a code change somewhere deep down resulted in a failure.
Playground link
@RyanCavanaugh I mean obviously that seems undecidable, but I still don’t see what it has to do with OP’s example of
type T2 = Record<string, T2>? Whether something like{foo: {}}is of typeT2seems perfectly decidable (not sure about deciding subtype/supertype relationships but that must not be a problem when T2 is defined asT2 = {[key: string]: T2}, so alias or not, the actual structure we’re talking about is well-defined)Imagine I told you that an integer was a flarpnumber if it was either prime or one of its digits was a flarpnumber. Is 6 a flarpnumber?