TypeScript Version: 3.7.x-dev.201xxxxx
Search Terms:
Readonly, type, annotation, alias, expand
Code
type Person = {
name: string;
}
type ReadOnlyPerson = Readonly<{
name: string;
}>
// hovering on x shows a type annotation of `Person`
let x: Person
// hovering on y shows a type annotation of Readonly<{ name: string }>
let y: 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
Playground Link:
http://www.typescriptlang.org/play/?ts=3.7-Beta&ssl=10&ssc=22&pln=1&pc=1#code/C4TwDgpgBAChBOBnA9gOygXigbwFBQKlQEMBbCALikWHgEtUBzAblwF9ddRIoAlCYgBMA8qgA2IOEjSY+AwWgkAePISJlK1WgxbsAfJzERgUAB5UpKVLiMmQVfkNETLaIA
Related Issues:
I wasn't able to find any bugs that looked similar, when through a couple of pages of issues using the search terms mentioned above.
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 https://github.com/microsoft/TypeScript/issues/32287#issuecomment-509414288
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.
Thanks for your comment @fatcerberus - I understand what you're saying.
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:
type ReadOnlyPerson = Readonly<{
name: string;
}>
type ReadOnlyInLinePerson = {
readonly name: string;
}
let y: ReadOnlyPerson
// hovering on z gives the expected type annotation -> z: ReadOnlyInLinePerson
let z: 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:
x as Person. This forces the Person type alias to be evaluated, which produces the concrete type { name: string }.{ name: string } was first constructed via the type expression Person, and displays that in IntelliSense whenever it encounters that same type in the future.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.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 declaring ReadOnlyPerson as:
type ReadOnlyPerson = Readonly<Person>
Now y will be annotated as Readonly<Person> in the hover text.
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.
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.
I'm not sure that @fatcerberus isn't right. This seems like something that "should" work
@RyanCavanaugh I found that if you redeclare ReadOnlyPerson as:
type ReadOnlyPerson = { readonly name: string }
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.
Repro (Playground):
type A<T> = Array<T>;
type B<T> = A<T>;
type C<T> = B<T>;
type D<T> = C<T>;
declare let x: D<any>; // x :: A<any>
Could be a possible workaround,
type Person = {
name: string;
}
type ReadOnlyPerson = Readonly<{
name: string;
}>
// hovering on x shows a type annotation of `Person`
let x: Person
// hovering on y shows a type annotation of Readonly<{ name: string }>
let y: ReadOnlyPerson
interface DoNotExpand extends Readonly<{
name: string;
}> {
}
//let z: DoNotExpand
let z: DoNotExpand;
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)
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).
Most helpful comment
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)