Subscribe on changes!

Typescript: Private fields in classes on data properties cause failed type verification since 3.0.8

avatar
May 22nd 2021

Version

3.0.11

Reproduction link

https://github.com/tonyfinn/vue-ts-error

Steps to reproduce

There are two projects in the reproduction example.

ts-type-error uses Vue 3.0.8 (but 3.0.11 is also broken). works uses Vue 3.0.7. The two projects are otherwise identical.

Both are using Vue CLI, so can be built by running npm run serve

What is expected?

Both projects would build or fail with the same result.

What is actually happening?

works builds correctly and runs correctly.

ts-type-error fails to build with the following error:

ERROR in src/App.vue:36:15
TS2345: Argument of type '{ readonly bar: string; log: () => void; }' is not assignable to parameter of type 'Foo'.
  Property 'baz' is missing in type '{ readonly bar: string; log: () => void; }' but required in type 'Foo'.
    34 |   methods: {
    35 |     clickHandler() {
  > 36 |       usesFoo(this.foo);
       |               ^^^^^^^^
    37 |     }
    38 |   }
    39 | })

The issue appears to be with the private fields. Inside the component methods, this.foo is not of type Foo as of 3.0.8, only of a type that is effectively the public fields/interface of Foo only. This means that when you use Foo as a type in a function declaration, passing this.foo is rejected, as it is missing the private fields as far as the type checker can see.

This appears to be a type checker issue only, if you change clickHandler to:


    clickHandler() {
      usesFoo((this.foo as any) as Foo);
    }

Then you can observe that the log messages are printed upon clicking the text at runtime.

avatar
May 22nd 2021

This is caused by the feature auto unwrap refs on public instance data .

type of data will be put in UnwrapNestedRefs<Data>, which will deeply unwrap the data object. So your type 'Foo' will be put in { [K in keyof T]: UnwrapRefSimple<T[K]> } as T.

After putting, type Foo will become { readonly bar: string; log: () => void; } and lose the private filed baz.

TypeScript is a structural type system. When we compare two different types, regardless of where they came from, if the types of all members are compatible, then we say the types themselves are compatible. However, when comparing types that have private and protected members, we treat these types differently. For two types to be considered compatible, if one of them has a private member, then the other must have a private member that originated in the same declaration. The same applies to protected members. link

The sturcture of type Foo is equal to { readonly bar: string; log: () => void; } , if you ignore the private fields. typescript won't ,so it break.

It seems that the break could not be fixed easily because of the feature auto unwrapping and limitation of typescript. Easiest way to solve this problem is usesFoo((this.foo as any) as Foo).

avatar
May 23rd 2021

@tonyfinn You can also use this.$data.foo, whose type is Foo. Nevertheless, that will disable auto unwrapping in type and cause inconsistency between static type and runtime type.

avatar
May 23rd 2021

you could close this issue. duplicate with #2981. see fix #3791.

avatar
May 23rd 2021

Duplicate of #2981