You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Wrapping type with Readonly causes type alias to be lost in annotation #34777
typePerson={name: string;}typeReadOnlyPerson=Readonly<{name: string;}>// hovering on x shows a type annotation of `Person`letx: Person// hovering on y shows a type annotation of Readonly<{ name: string }>lety: ReadOnlyPerson
Expected behavior:
Type aliases should be preserved in annotations when using the Readonly type. In the example above I would expect the type annotation for y to be ReadOnlyPerson
Actual behavior:
Type alias is lost in the annotation when using the Readonly type. The type annotation for y in the example above is Readonly<{ name: string } instead of ReadOnlyPerson
IIRC type aliases are not directly preserved in the way you think, TS just remembers if the first instantiation of a type was due to a type alias and back-maps it to the alias if so. Since this is just a heuristic, there are a lot of ways it can go wrong. For further elaboration see #32287 (comment)
In short: type aliases are not first-class types. They are more like macros where the compiler expands the alias and uses the expansion directly, which often loses information on where the type originated.
What confuses is me is that the Readonly type doesn't do what I expect in this case. Since the type alias is defined right there, I don't see why the compiler would lose information on where the type originated. I guess what I would expect is for these two to be equivalent:
typeReadOnlyPerson=Readonly<{name: string;}>typeReadOnlyInLinePerson={readonlyname: string;}lety: ReadOnlyPerson// hovering on z gives the expected type annotation -> z: ReadOnlyInLinePersonletz: ReadOnlyInLinePerson
Edit: I guess the workaround would be to not use the Readonly type and just write readonly on every line manually instead and it would have the same effect, but it would be nice to avoid that if possible :)
It confused me at first, too, but here's what I think happens:
You declare x as Person. This forces the Person type alias to be evaluated, which produces the concrete type { name: string }.
From here you're good: TS remembers that { name: string } was first constructed via the type expression Person, and displays that in IntelliSense whenever it encounters that same type in the future.
You declare y as ReadOnlyPerson. TS evaluates ReadOnlyPerson and gets Readonly<{ name: string }>. This is not yet a first-class type as it contains the Readonly type alias, so must be further evaluated.
This is a concrete type, so TS remembers that it was produced by the type expression Readonly<{ name: string }> and displays that in IntelliSense whenever it encounters the same type in the future.
Because there was a second type alias in the way, information about the first one--ReadOnlyPerson--has been lost.
I do wonder why it doesn't display as Readonly<Person>, though. This is just speculation but I'm thinking TS remembers the exact source text of the type expression that created the type, rather than storing the type parameters individually. You can see this by declaring ReadOnlyPerson as:
typeReadOnlyPerson=Readonly<Person>
Now y will be annotated as Readonly<Person> in the hover text.
It would be a bit cumbersome to define the type and the read-only version separately each time in order to improve the intellisense. Let's see what the official TS maintainers have to say about this but I suspect this is not something that can be easily remedied.
In short: type aliases are not first-class types. They are more like macros where the compiler expands the alias and uses the expansion directly, which often loses information on where the type originated.
This is confusing and non-intuitive to me. I think at the very least this should be documented, but if the contract can be improved, or if some control can be afforded via other keywords, that would be a huge help.
then that fixes the issue and y as typed as ReadOnlyPerson in the hover text. It seems that if there is any nesting of type aliases, TS only takes the "innermost" one for the name of the final type. It might also explain all those issues where Omit<> expands to a huge Pick<> type in the hover.
typePerson={name: string;}typeReadOnlyPerson=Readonly<{name: string;}>// hovering on x shows a type annotation of `Person`letx: Person// hovering on y shows a type annotation of Readonly<{ name: string }>lety: ReadOnlyPersoninterfaceDoNotExpandextendsReadonly<{name: string;}>{}//let z: DoNotExpandletz: DoNotExpand;
The compiler's behaves as intended here. Instantiations of generic type aliases (such as Readonly<T>) are cached and shared based on the type identities of the type arguments. In other words, every reference to Readonly<Foo> in a program ends up referencing the exact same type object. This type of sharing saves both time and memory, but it also precludes associating aliases with particular instantiations (because there could well be multiple possible aliases).
Reacted by Christos Dimitroulas and Ayub Begimkulov
Activity
fatcerberus commentedon Oct 29, 2019
IIRC type aliases are not directly preserved in the way you think, TS just remembers if the first instantiation of a type was due to a type alias and back-maps it to the alias if so. Since this is just a heuristic, there are a lot of ways it can go wrong. For further elaboration see #32287 (comment)
In short: type aliases are not first-class types. They are more like macros where the compiler expands the alias and uses the expansion directly, which often loses information on where the type originated.
cdimitroulas commentedon Oct 29, 2019
Thanks for your comment Bruce Pascoe (@fatcerberus) - I understand what you're saying.
What confuses is me is that the
Readonlytype doesn't do what I expect in this case. Since the type alias is defined right there, I don't see why the compiler would lose information on where the type originated. I guess what I would expect is for these two to be equivalent:Edit: I guess the workaround would be to not use the
Readonlytype and just writereadonlyon every line manually instead and it would have the same effect, but it would be nice to avoid that if possible :)fatcerberus commentedon Oct 29, 2019
It confused me at first, too, but here's what I think happens:
xasPerson. This forces thePersontype alias to be evaluated, which produces the concrete type{ name: string }.{ name: string }was first constructed via the type expressionPerson, and displays that in IntelliSense whenever it encounters that same type in the future.yasReadOnlyPerson. TS evaluatesReadOnlyPersonand getsReadonly<{ name: string }>. This is not yet a first-class type as it contains theReadonlytype alias, so must be further evaluated.Readonly<{ name: string }>and gets{ readonly name: string }.Readonly<{ name: string }>and displays that in IntelliSense whenever it encounters the same type in the future.ReadOnlyPerson--has been lost.I do wonder why it doesn't display as
Readonly<Person>, though. This is just speculation but I'm thinking TS remembers the exact source text of the type expression that created the type, rather than storing the type parameters individually. You can see this by declaringReadOnlyPersonas:Now
ywill be annotated asReadonly<Person>in the hover text.cdimitroulas commentedon Oct 29, 2019
Interesting, that makes sense.
It would be a bit cumbersome to define the type and the read-only version separately each time in order to improve the intellisense. Let's see what the official TS maintainers have to say about this but I suspect this is not something that can be easily remedied.
craigkovatch commentedon Oct 30, 2019
This is confusing and non-intuitive to me. I think at the very least this should be documented, but if the contract can be improved, or if some control can be afforded via other keywords, that would be a huge help.
RyanCavanaugh commentedon Nov 8, 2019
I'm not sure that Bruce Pascoe (@fatcerberus) isn't right. This seems like something that "should" work
fatcerberus commentedon Nov 8, 2019
Ryan Cavanaugh (@RyanCavanaugh) I found that if you redeclare
ReadOnlyPersonas:then that fixes the issue and
yas typed asReadOnlyPersonin the hover text. It seems that if there is any nesting of type aliases, TS only takes the "innermost" one for the name of the final type. It might also explain all those issues whereOmit<>expands to a hugePick<>type in the hover.Repro (Playground):
AnyhowStep commentedon Nov 8, 2019
Could be a possible workaround,
Playground
I've only ever wanted to force TS to expand types. I've never wanted to force TS to alias types.
So, this is all new to me =x
(Now I have h4xx to force TS to expand and not-expand types, yay)
ahejlsberg commentedon Nov 18, 2019
The compiler's behaves as intended here. Instantiations of generic type aliases (such as
Readonly<T>) are cached and shared based on the type identities of the type arguments. In other words, every reference toReadonly<Foo>in a program ends up referencing the exact same type object. This type of sharing saves both time and memory, but it also precludes associating aliases with particular instantiations (because there could well be multiple possible aliases).fixup readonly, fixup query types