-
-
Notifications
You must be signed in to change notification settings - Fork 8.5k
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
Class API RFC Draft (for internal feedback) #20
Comments
Looks good, my only concern right now is regarding Two Ways of Doing the Same Thing I don't think there is a solution to this, we cannot move forward if we do not adopt new syntaxes, and it definitely improves typings, which also improves dev experience for regular devs, I'm more rising a concern I have |
Is there any plan to add type-checking for events? type Props = {
tmp: string
alpha?: boolean
}
type Events = {
click: string
something: boolean
a: number
b: void
}
type ScopedSlots = {
s1?: {
click?: string
attr: boolean
}
s2?: {
s: null
}
s3: {
a: 'tst'
}
}
class Cmp extends Vue<Props, Events, ScopedSlots> {
created() {
console.log(this.$props.alpha)
this.$emit('a', 2)
this.$emit('b')
}
}
const h = <Cmp tmp="al" on={{ b: () => console.log() }} scopedSlots={{ s3: (props) => <a /> }} /> Can we consider full TSX support in core with v3? |
@nickmessing yes, you should consider improving this file: https://github.com/vuejs/vue-next/blob/master/packages/runtime-dom/jsx.d.ts |
I just realized an important drawback of using decorators for props would be TSX inference :/ |
@yyx990803, I will release an alpha of TSX support for Vue 2 and then will try to port that to Vue 3. |
I like it and in the basic form, it looks relatively easy even for people without TS, React or Angular experience. I can imagine two questions being asked by potential users:
But in both cases it's too early for that. |
I have the same hesitation as @posva regarding fragmentation and complexity. Even within the class implementation, we'd have different best practices and caveats depending on whether native classes, Babel classes, or TypeScript classes are used. We're also warning people not to use some features of classes in some or all cases, like constructors and decorators. I doubt we're even fully aware of all the caveats and edge cases, especially since more changes are likely to come. The fragmentation and complexity also places a greater burden on library authors, not only in terms of support and possible edge cases, but also documentation. It makes me wonder whether we should strongly discourage everyone except TypeScript users from using classes, at least until the features we'd rely on are fully stable and implementations more unified. It's really a shame there's been no response from the TypeScript team regarding @octref's proposed addition to the TypeScript toolchain, since I think it would solve all our problems regarding TypeScript. We could provide an even better typing experience than this, without having to deal with classes while they're still stabilizing. Should we try to push the TypeScript team harder on that? |
There really is only one difference, that is the usage of decorator for props in TypeScript. The proposal intentionally covers all possible caveats in as much details as possible, but in practice all of them have very little impact on actual DX.
|
@yyx990803 What do you think about that addition to the TypeScript toolchain though? Should we try to push harder on that before trying to balance the compromises of making Vue more complex? |
class MyComponent extends Vue {
@prop foo: number = 1
bar = this.foo + 1
} I believe you can hijack the setter of I do have other concerns and I'll pass them along after discussing with @DanielRosenwasser next week. |
It'll help Vetur to do auto completion and diagnostic error checking, but I think that's not necessarily a solution to the motivation on:
I'm not saying a better typing experience for the object based usage is not worth working on. I'll still work on it, and I can see it becoming doable. |
I'd like to see if the issue with private can be fixed. I'm wondering, would it be reasonable for the I'd like to try this out and test it. Are there any example programs using Vue 3 that I could base this on? |
@nicolo-ribaudo , who has been working on the implementation of private and decorators in Babel, has volunteered to help out with looking into whether Vue could use the pattern in #20 (comment) . Is there any way he could get access to this repository? |
@littledan nice! I just did a quick experiment and it seems it would indeed work and solve quite a few issues in our current implementation. I'll try to rework the current prototype to use this strategy to confirm that it fully resolves the issues. |
@littledan great news - confirmed that this can indeed work. This also solves all the problems related to |
I'm very happy to hear this! Maybe we can work together on documenting this strategy well enough so that other framework authors can learn from this design. |
So the decorator-less TS equivalent of this: import { prop } from '@vue/decorators'
class MyComponent extends Vue {
@prop count: number
} Would be this: interface Props {
count: number;
}
class MyComponent extends Vue<Props> {
static props = {
count: Number;
}
} But for class MyComponent extends Vue {
@prop foo: ComplexType;
} Is the equivalent below? interface Props {
foo: ComplexType; // but this types `this.$props.foo` not `this.foo`
}
class MyComponent extends Vue<Props> {
static props = {
foo: Object as Prop<ComplexType>
}
} So if you want to keep accessing props on But then why should I still write the class MyComponent extends Vue<Props> {
static props = {
foo: Object as Prop<ComplexType>
}
} |
For the following class MyComponent extends Vue {
@prop foo: ComplexType;
} This doesn't provide types for On the other hand, I don't know if we can keep the inference working with the following: class MyComponent extends Vue {
static props = {
foo: Object as Prop<ComplexType>
}
} This works for Object-based API by inferring it as an argument for If a prop is already exposed and properly typed on |
So I spoke with @octref a bit about the current proposal. On the whole, data, computed, and methods all seem great. The only strange piece is props for TypeScript users. Here's a bit of the friction that users will likely run into that you may want to consider. Issues with decoratorsWhile decorators make internal type-checking easy, they don't solve the problem for external type-checking. Given that one of the ideas was to allow Vue to work better with JSX syntax or class SpecialButton extends Vue.Component {
@prop text: string;
static template = `
<button>
I am a special button!
{{text}}
</button>
`;
}
class App {
render(h) {
// How does this get type-checked?
return h(SpecialButton, {
text: "click",
});
}
static components = {
SpecialButton,
};
} The solution is to create another interface. interface Props {
text: string;
}
class SpecialButton extends Vue.Component<Props> {
@prop text: string;
static template = `
<button>
I am a special button!
{{text}}
</button>
`;
} It may be reasonable to say "well, templates are the primary way users will consume these either, and we'll always be able to read the source, so users who use render methods can opt in here", but that's not entirely true and there are caveats to that. Anything that tries to check templates will inevitably have to deal with components in npm which are already compiled. Issues with the schemaThe The most reasonable thing to do with the schema is to just define the names of each prop instead of defining the respective validators. interface Props {
text: string;
}
class SpecialButton extends Vue.Component<Props> {
static props = ["text"];
} Issues with no schema/decoratorsBy default, users who provide no schema for interface Props {
text: string;
}
class SpecialButton extends Vue.Component<Props> {
static template = `
<button>
I am a special button!
{{$props.text}}
</button>
`;
} This feels a little strange to explain to a user, and it can be a pain to refactor to if you already have a I think the best experience I can imagine is just interface Props {
text: string;
}
class SpecialButton extends Vue.Component<Props> {
static template = `
<button>
I am a special button!
{{text}}
</button>
`;
} but I know that there are concerns over accidentally defining an instance member with the same name as a prop. |
@DanielRosenwasser thanks for the feedback! I agree that ideally, a single interface would offer a decent dev experience and covers both internal and external type checking, however, our current constraints are:
So it seems the main downside of decorators is that it doesn't work well with TSX. Is the generic argument a hard requirement for TSX to work? Maybe instead of interface ElementAttributesDecorator {
'@prop': {}
} Alternatively, is there anyway to infer |
@yyx990803 You might want to look into Stencil.
What do you think about dropping
IMO |
@octref interesting, so the RE dropping
interface ElementAttributesProperty {
$props: {}
} But technically, this doesn't have to be done via a generic argument... the user can just declare The problem with inferring from So if we can find a way to generate the proper type augmentations (basically auto-generate proper |
Summary
Introduce built-in support for authoring components as native ES2015 classes.
Basic example
Motivation
Vue's current object-based component API has created some challenges when it comes to type inference. As a result, most users opting into using Vue with TypeScript end up using vue-class-component. This approach works, but with some drawbacks:
Internally, Vue 2.x already represents each component instance with an underlying "class". We are using quotes here because it's not using the native ES2015 syntax but the ES5-style constructor/prototype function. Nevertheless, conceptually components are already handled as classes internally.
vue-class-component
had to implement some inefficient workarounds in order to provide the desired API without altering Vue internals.vue-class-component
has to maintain typing compatibility with Vue core, and the maintenance overhead can be eliminated by exposing the class directly from Vue core.The primary motivation of native class support is to provide a built-in and more efficient replacement for
vue-class-component
. The affected target audience are most likely also TypeScript users.The API is also designed to not rely on anything TypeScript specific: it should work equally well in plain ES, for users who prefer using native ES classes.
Note we are not pushing this as a replacement for the existing object-based API - the object-based API will continue to work in 3.0.
Detailed design
Basics
A component can be declared by extending the base
Vue
class provided by Vue core:Data
Reactive instance data properties can be declared using class fields syntax (stage 3):
This is currently supported in Chrome stable 72+ and TypeScript. It can also be transpiled using Babel. If using native ES classes without any transpilation, it's also possible to manually set
this.count = 0
inconstructor
, which would in turn require asuper()
call:This is verbose and also has incorrect semantics (see below). A less verbose alternative is using the special
data()
method, which works the same as in the object-based syntax:A Note on
[[Set]]
vs[[Define]]
The class field syntax uses
[[Define]]
semantics in both native and transpiled implementations (Babel already conforms to the latest spec and TS will have to follow suite). This meanscount = 0
in the class body is executed with the semantics ofObject.defineProperty
and will always overwrite a property of the same name inherited from a parent class, regardless of whether it has a setter or not.In comparison,
this.count = 0
in constructor is using[[Set]]
semantics - if the parent class has a defined setter namedcount
, the operation will trigger the setter instead of overwriting the definition.For Vue's API,
[[Define]]
is the correct semantics, since an extended class declaring a data property should overwrite a property with the same name on the parent class.This should be a very rare edge case since most users will likely be using the class field syntax either natively or via a transpiler with correct semantics, or using the
data()
alternative.Lifecycle Hooks
Built-in lifecycle hooks should be declared directly as methods, and works largely the same with their object-based counterparts:
Props
In v3, props declarations can be optional. The behavior will be different based on whether props are declared.
Props with Explicit Declaration
Props can be declared using the
props
static property (static properties are used for all component options that do not have implicit mapping). When props are declared, they can be accessed directly onthis
:Similar to v2, any attributes passed to the component but is not declared as a prop will be exposed as
this.$attrs
. Note that the non-props attribute fallthrough behavior will also be adjusted - it is discussed in more details in a separate RFC.Props without Explicit Declaration
It is possible to omit props declarations in v3. When there is no explicit props declaration, props will NOT be exposed on
this
- they will only be available onthis.$props
:Inside templates, the prop also must be accessed with the
$props
prefix, .e.g.{{ $props.msg }}
.Any attribute passed to this component will be exposed in
this.$props
. In addition,this.$attrs
will be simply pointing tothis.$props
since they are equivalent in this case.Computed Properties
Computed properties are declared as getter methods:
Note although we are using the getter syntax, these functions are not used a literal getters - they are converted into Vue computed properties internally with dependency-tracking-based caching.
Methods
Any method that is not a reserved lifecycle hook is considered a normal instance method:
When methods are accessed from
this
, they are automatically bound to the instance. This means there is no need to worry about callingthis.foo = this.foo.bind(this)
.Other Options
Other options that do not have implicit mapping in the class syntax should be declared as static class properties:
The above syntax requires static class fields (stage 3). In non-supporting environment, manual attaching is required:
Or:
TypeScript Usage
In TypeScript, since
data
properties are declared using class fields, the type inference just works:For props, we intend to provide a decorator that internally transforms decorated fields in to corresponding runtime options (similar to the
@Prop
decorator invue-property-decorators
):This is equivalent to the following in terms of runtime behavior (only static type checking, no runtime checks):
The decorator can also be called with additional options for more specific runtime behavior:
Note on Prop Default Value
Note that due to the limitations of the TypeScript decorator implementation, we cannot use the following to declare default value for a prop:
The culprit is the following case:
If the parent component passes in the
foo
prop, the default value of1
should be overwritten. However, the way TypeScript transpiles the code places the two lines together in the constructor of the class, giving Vue no chance to overwrite the default value properly. Vue will throw a warning when such usage is detected.Instead, use the decorator option to declare default values:
This restriction can be lifted in the future when the ES decorators proposal has been finalized and TS has been updated to match the spec, assuming the final spec does not deviate too much from how it works now.
$props
and$data
To access
this.$props
orthis.$data
in TypeScript, the baseVue
class accepts generic arguments:Mixins
Mixins work a bit differently with classes, primarily to ensure proper type inference:
If type inference is needed, mixins must be declared as classes extending the base
Vue
class (otherwise, the object format also works).To use mixins, the final component should extend a class created from the
mixins
method instead of the baseVue
class.The class returned from
mixins
also accepts the same generics arguments as the baseVue
class.Difference from 2.x Constructors
One major difference between 3.0 classes and the 2.x constructors is that they are not meant to be instantiated directly. i.e. you will no longer be able to do
new MyComponent({ el: '#app' })
to mount it - instead, the instantiation/mounting process will be handled by separate, dedicated APIs. In cases where a component needs to be instantiated for testing purposes, corresponding APIs will also be provided. This is largely due to the internal changes where we are moving the mounting logic out of the component class itself for better decoupling, and also has to do our plan to redesign the global API for bootstrapping an app.Drawbacks
Reliance on Stage 2/3 Language Features
Class Fields
The proposed syntax relies on two currently stage-3 proposals related to class fields:
These are required to achieve the ideal usage. Although there are workarounds in cases where they are not available, the workarounds result in sub-optimal authoring experience.
If the user uses Babel or TypeScript, these can be covered. Luckily these two combined should cover a pretty decent percentage of all users. For learning / prototyping usage without compile steps, browsers with native support (e.g. Chrome Canary) can also be used.
There is a small risk since these proposals are just stage 3, and are still being actively debated on - technically, there are still chances that they get further revised or even dropped. The good news is that the parts that are relevant here doesn't seem likely to change. There was a somewhat related debate regarding the semantics of class fields being
[[Set]]
vs[[Define]]
, and it has been settled as[[Define]]
which in my opinion is the preferred semantics for this API.Decorators
The TypeScript usage relies on decorators. The decorators proposal for JavaScript is still stage 2 and undergoing major revisions - it's also completely different from how it is implemented in TS today (although TS is expected to match the proposal once it is finalized). Its latest form just got rejected from advancing to stage 3 at TC39 due to concerns from JavaScript engine implementors. It is thus still quite risky to design the API around decorators at this point.
Before ES decorators are finalized, we only recommend using decorators in TypeScript.
The decision to go with decorators for props in TypeScript is due to the following:
Decorators is the only option that allows us to express both static and runtime behavior in the same syntax, without the need for double declaration. This is discussed in more details in the Alternatives section.
Both the current TS implementation and the current stage 2 proposal can support the desired usage.
It's also highly likely that the finalized proposal is going to support the usage as well. So even after the proposal finalizes and TS' implementation has been updated to match the proposal, the API can continue to work without syntax changes.
The decorator-based usage is opt-in and built on top of the
static props
based usage. So even if the proposal changes drastically or gets abandoned we still have something to fallback to.If users are using TypeScript, they already have decorators available to them via TypeScript's tool chain so unlike vanilla JavaScript there's no need for additional tooling.
this
Identity inconstructor
In Vue 3 component classes, the
this
context in all lifecycle hooks and methods are in fact a Proxy to the actual underlying instance. This Proxy is responsible for returning proper values for the data, props and computed properties defined on the current component, and provides runtime warning checks. It is important for performance reasons as it avoids many expensiveObject.defineProperty
calls when instantiating components.In practice, your code will work exactly the same - the only cases where you need to pay attention is if you are using
this
inside the nativeconstructor
- this is the only place where Vue cannot swap the identity ofthis
so it will not be equal to thethis
exposed everywhere else:In practice, there shouldn't be cases where you must use the
constructor
, so the best practice is to simply avoid it and always use component lifecycle hooks.Two Ways of Doing the Same Thing
This may cause beginners to face a choice early on: to go with the object syntax, or the class syntax?
For users who already have a preference, it is not really an issue. The real issue is that for beginners who are not familiar with classes, the syntax raises the learning barrier. In the long run, as ES classes stabilize and get more widely used, it may eventually become a basic pre-requisite for all JavaScript users, but now is probably not the time yet.
One way to deal with it is providing examples for both syntaxes in the new docs and allow switching between them. This allows users to pick a preferred syntax during the learning process.
Alternatives
Options via Decorator
This is similar to
vue-class-component
but it requires decorators - and as mentioned, it is only stage 2 and risky to rely on. We are using decorators for props, but it's primarily for better type-inference and only recommended in TypeScript. For now we should avoid decorators in plain ES as much as possible.Declaring Prop Types via Generic Arguments
For declaring prop types in TypeScript, we considered avoiding decorators by merging the props interface passed to the class as a generic argument on to the class instance:
However, this creates a mismatch between the typing and the runtime behavior. Because there is no runtime declaration for the
msg
prop, it will not be exposed onthis
. To make the types and runtime consistent, we end up with a double-declaration:We also considered eliminating the need for double-declaration via tooling - e.g. Vetur can pre-transform the interface into equivalent runtime declaration, or vice-versa, so that only the interface or the
static props
declaration is needed. However, both have drawbacks:The interface cannot enforce runtime type checking or custom validation;
The
static props
runtime declaration cannot facilitate type inference for advanced type shapes.Decorators is the only option the can unify both in the same syntax:
Adoption strategy
This does not break existing usage, but rather introduces an alternative way of authoring components. TypeScript users, especially those already using
vue-class-component
should have no issue grasping it. For beginners, we should probably avoid using it as the default syntax in docs, but we should provide the option to switching to it in code examples.For existing users using TypeScript and
vue-class-component
, a simple migration strategy would be shipping a build ofvue-class-component
that provides a@Component
decorator that simply spreads the options on to the class. Since the required change is pretty mechanical, a code mod can also be provided.The text was updated successfully, but these errors were encountered: