Typescript: Private fields in classes on data properties cause failed type verification since 3.0.8
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.
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)
.
@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.