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

Wrapping type with Readonly causes type alias to be lost in annotation #34777

Closed
cdimitroulas opened this issue Oct 28, 2019 · 9 comments
Closed
Assignees
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@cdimitroulas
Copy link

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.

@fatcerberus
Copy link

fatcerberus commented 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
Copy link
Author

cdimitroulas commented Oct 29, 2019

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

@fatcerberus
Copy link

It confused me at first, too, but here's what I think happens:

  1. You declare x as Person. This forces the Person type alias to be evaluated, which produces the concrete type { name: string }.
  2. 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.
  3. 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.
  4. TS evaluates Readonly<{ name: string }> and gets { readonly name: string }.
  5. 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.
  6. 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:

type ReadOnlyPerson = Readonly<Person>

Now y will be annotated as Readonly<Person> in the hover text.

@cdimitroulas
Copy link
Author

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
Copy link

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.

@RyanCavanaugh RyanCavanaugh added the Needs Investigation This issue needs a team member to investigate its status. label Nov 8, 2019
@RyanCavanaugh RyanCavanaugh added this to the TypeScript 3.8.0 milestone Nov 8, 2019
@RyanCavanaugh
Copy link
Member

I'm not sure that @fatcerberus isn't right. This seems like something that "should" work

@fatcerberus
Copy link

@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>

@AnyhowStep
Copy link
Contributor

AnyhowStep commented Nov 8, 2019

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;

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
Copy link
Member

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

@ahejlsberg ahejlsberg added Working as Intended The behavior described is the intended behavior; this is not a bug and removed Needs Investigation This issue needs a team member to investigate its status. labels Nov 18, 2019
@ahejlsberg ahejlsberg removed this from the TypeScript 3.8.0 milestone Nov 18, 2019
tantaman added a commit to tantaman/strut that referenced this issue Nov 13, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

6 participants