preface
Although Vue 2 is said to be not good enough for TypeScript support and cumbersome to use, there are still a lot of things that Vue does to support TypeScript. There is also an official solution github.com/vuejs/vue-c… Of course, vue-class-Component doesn’t just have better support for types. At its core, it writes vUE components as classes.
Today we’re going to focus less on vue-class-Component and more on what Vue 2 itself does with type support. Let’s learn how to do type support for a project that isn’t written in TS and what we can learn from it for our own scenarios.
The text analysis
What
Type support is TypeScript support, which is a partial understanding, but it can be understood as such. So what exactly is TypeScript support, and what does it do?
The first thing to know about TypeScript is that it is JavaScript with type syntax. The official TypeScript website is available at www.typescriptlang.org/. TypeScript provides JavaScript with a static typing system that really helps prevent many runtime errors; In addition, IDE support, coupled with a static typing system, gives developers good intelligence and makes stacking code much faster.
The core of Vue support for TypeScript is that TypeScript correctly extrapolate Vue and developer-defined component types so that developers can use it with type extrapolate. Detailed introduction articles can be found at cn.vuejs.org/v2/guide/ty…
With TypeScript support in place, we can write components that look something like this:
As you can see, properties on this, such as the type Prop, are properly alerted, as are methods like $nextTick on the prototype.
How
So how does this support work? Let’s take a look at the processing of Vue.
Added a type entry declaration
Source file at github.com/vuejs/vue/b… . Following the TypeScript specification, add a typings declaration in package.json pointing to the type declaration file.
Type declaration file
Declaration file, github.com/vuejs/vue/b… , here to see the suffix which s, this is also a Feature in the TypeScript – type declaration documents, there are detailed introduction www.typescriptlang.org/docs/handbo… .
TypeScript type support can be achieved by adding type-specific declarations to projects that do not have TS declarations. The classic project is github.com/DefinitelyT… .
Let’s take a look at the contents of index.d.ts:
import { Vue } from "./vue";
import "./umd";
// Expose Vue
export default Vue;
export {
CreateElement,
VueConstructor
} from "./vue";
export {
Component,
AsyncComponent,
ComponentOptions,
FunctionalComponentOptions,
RenderContext,
PropType,
PropOptions,
ComputedOptions,
WatchHandler,
WatchOptions,
WatchOptionsWithHandler,
DirectiveFunction,
DirectiveOptions
} from "./options";
export {
PluginFunction,
PluginObject
} from "./plugin";
export {
VNodeChildren,
VNodeChildrenArrayContents,
VNode,
VNodeComponentOptions,
VNodeData,
VNodeDirective
} from "./vnode";
Copy the code
Also, modules were split and exported to the exposed modules. Here we focus on two modules:./vue module and./options module.
Take a look at the core github.com/vuejs/vue/b…
import {
Component,
AsyncComponent,
ComponentOptions,
FunctionalComponentOptions,
WatchOptionsWithHandler,
WatchHandler,
DirectiveOptions,
DirectiveFunction,
RecordPropsDefinition,
ThisTypedComponentOptionsWithArrayProps,
ThisTypedComponentOptionsWithRecordProps,
WatchOptions,
} from "./options";
import { VNode, VNodeData, VNodeChildren, NormalizedScopedSlot } from "./vnode";
import { PluginFunction, PluginObject } from "./plugin";
export interface CreateElement {
// $createElement, where the function interface is overloaded
// Distinguish between cases with data and cases without data(tag? :string | Component<any.any.any.any> | AsyncComponent<any.any.any.any> | (() = >Component), children? : VNodeChildren): VNode; (tag? :string | Component<any.any.any.any> | AsyncComponent<any.any.any.any> | (() = >Component), data? : VNodeData, children? : VNodeChildren): VNode; }// Vue instance type
export interface Vue {
// Various API definitions
readonly $el: Element;
readonly $options: ComponentOptions<Vue>;
readonly $parent: Vue;
readonly $root: Vue;
readonly $children: Vue[];
readonly $refs: { [key: string]: Vue | Element | (Vue | Element)[] | undefined };
readonly $slots: { [key: string]: VNode[] | undefined };
readonly $scopedSlots: { [key: string]: NormalizedScopedSlot | undefined };
readonly $isServer: boolean;
readonly $data: Record<string.any>;
readonly $props: Record<string.any>;
readonly $ssrContext: any;
readonly $vnode: VNode;
readonly $attrs: Record<string.string>;
readonly $listeners: Record<string.Function | Function[] >;// Support the chained call return value this$mount(elementOrSelector? : Element |string, hydrating? :boolean) :this;
$forceUpdate(): void;
$destroy(): void;
// Use typeof to get the Vue. Set type directly
$set: typeof Vue.set;
$delete: typeof Vue.delete;
// $watch function overload
$watch(
expOrFn: string.callback: (this: this, n: any, o: any) = > void, options? : WatchOptions ): (() = > void);
// Type automatic matching, get the generic T
$watch<T>(
expOrFn: (this: this) = > T,
callback: (this: this, n: T, o: T) = > void, options? : WatchOptions ): (() = > void);
$on(event: string | string[].callback: Function) :this;
$once(event: string | string[].callback: Function) :this; $off(event? :string | string[], callback? :Function) :this;
$emit(event: string. args:any[]) :this;
$nextTick(callback: (this: this) = > void) :void;
$nextTick(): Promise<void>;
$createElement: CreateElement;
}
// Merge the values of data methods and so on into this context
export type CombinedVueInstance<Instance extends Vue, Data, Methods, Computed, Props> = Data & Methods & Computed & Props & Instance;
// The final Vue constructor for the vue. extend pattern
export type ExtendedVue<Instance extends Vue, Data, Methods, Computed, Props> = VueConstructor<CombinedVueInstance<Instance, Data, Methods, Computed, Props> & Vue>;
// vue. config Specifies the configuration items that can be configured
export interface VueConfiguration {
silent: boolean;
optionMergeStrategies: any;
devtools: boolean;
productionTip: boolean;
performance: boolean;
errorHandler(err: Error.vm: Vue, info: string) :void;
warnHandler(msg: string.vm: Vue, trace: string) :void;
ignoredElements: (string | RegExp) []; keyCodes: { [key:string] :number | number[]}.async: boolean;
}
// Vue constructor
export interface VueConstructor<V extends Vue = Vue> {
// The new constructor is overloaded. This is the type declaration provided by TS for creating instances using new
// https://www.typescriptlang.org/docs/handbook/2/functions.html#construct-signatures
new <Data = object, Methods = object, Computed = object, PropNames extends string = never>(options? : ThisTypedComponentOptionsWithArrayProps<V, Data, Methods, Computed, PropNames>): CombinedVueInstance<V, Data, Methods, Computed, Record<PropNames,any> >;// ideally, the return type should just contain Props, not Record<keyof Props, any>. But TS requires to have Base constructors with the same return type.
new <Data = object, Methods = object, Computed = object, Props = object>(options? : ThisTypedComponentOptionsWithRecordProps<V, Data, Methods, Computed, Props>): CombinedVueInstance<V, Data, Methods, Computed, Record<keyof Props,any> >;new(options? : ComponentOptions<V>): CombinedVueInstance<V,object.object.object, Record<keyof object.any> >; extend<Data, Methods, Computed, PropNamesextends string = never>(options? : ThisTypedComponentOptionsWithArrayProps<V, Data, Methods, Computed, PropNames>): ExtendedVue<V, Data, Methods, Computed, Record<PropNames,any> >; extend<Data, Methods, Computed, Props>(options? : ThisTypedComponentOptionsWithRecordProps<V, Data, Methods, Computed, Props>): ExtendedVue<V, Data, Methods, Computed, Props>; extend<PropNamesextends string = never>(definition: FunctionalComponentOptions<Record<PropNames, any>, PropNames[]>): ExtendedVue<V, {}, {}, {}, Record<PropNames, any> >; extend<Props>(definition: FunctionalComponentOptions<Props, RecordPropsDefinition<Props>>): ExtendedVue<V, {}, {}, {}, Props>; extend(options? : ComponentOptions<V>): ExtendedVue<V, {}, {}, {}, {}>; nextTick<T>(callback:(this: T) = > void, context? : T):void;
nextTick(): Promise<void>
set<T>(object: object.key: string | number.value: T): T;
set<T>(array: T[], key: number.value: T): T;
delete(object: object.key: string | number) :void;
delete<T>(array: T[], key: number) :void;
directive(
id: string, definition? : DirectiveOptions | DirectiveFunction ): DirectiveOptions; filter(id:string, definition? :Function) :Function;
component(id: string): VueConstructor;
component<VC extends VueConstructor>(id: string.constructor: VC): VC;
component<Data, Methods, Computed, Props>(id: string.definition: AsyncComponent<Data, Methods, Computed, Props>): ExtendedVue<V, Data, Methods, Computed, Props>;
component<Data, Methods, Computed, PropNames extends string = never>(id: string, definition? : ThisTypedComponentOptionsWithArrayProps<V, Data, Methods, Computed, PropNames>): ExtendedVue<V, Data, Methods, Computed, Record<PropNames,any> >; component<Data, Methods, Computed, Props>(id:string, definition? : ThisTypedComponentOptionsWithRecordProps<V, Data, Methods, Computed, Props>): ExtendedVue<V, Data, Methods, Computed, Props>; component<PropNamesextends string>(id: string.definition: FunctionalComponentOptions<Record<PropNames, any>, PropNames[]>): ExtendedVue<V, {}, {}, {}, Record<PropNames, any> >; component<Props>(id:string.definition: FunctionalComponentOptions<Props, RecordPropsDefinition<Props>>): ExtendedVue<V, {}, {}, {}, Props>;
component(id: string, definition? : ComponentOptions<V>): ExtendedVue<V, {}, {}, {}, {}>; use<T>(plugin: PluginObject<T> | PluginFunction<T>, options? : T): VueConstructor<V>; use(plugin: PluginObject<any> | PluginFunction<any>, ...options: any[]): VueConstructor<V>;
mixin(mixin: VueConstructor | ComponentOptions<Vue>): VueConstructor<V>;
compile(template: string): {
render(createElement: typeof Vue.prototype.$createElement): VNode;
staticRenderFns: (() = > VNode)[];
};
observable<T>(obj: T): T;
util: {
warn(msg: string, vm? : InstanceType<VueConstructor>):void;
};
config: VueConfiguration;
version: string;
}
// Exposes a Vue value of type VueConstructor
export const Vue: VueConstructor;
Copy the code
So let’s look at some of the things that are going on here.
- Two Vue puzzles, we need to distinguish the top one
interface Vue
Declared as a type, the actual meaning is the Vue instance, which is finally exposedconst Vue
Is a value that is exposed by the declaration. This value is of typeVueConstructor
The Vue constructor. TS will automatically decide which meaning to use based on the scenario you are using- First, you need to know that exist in the TS type Namespaces, Types, Values Values, namespace their meanings: www.typescriptlang.org/docs/handbo…
- In TS merger is to support this way, because they although two of the same name, but different meanings, related documents www.typescriptlang.org/docs/handbo…
- And you can also tell from the TS documentation that this behavior is the same as when you declare a class,
class C
Correspondingly, two declarations are created in TS: one calledC
The corresponding type is an instance of C, and the other is the value of C, which is the constructor function pointing to. Class is kind of a built-in processingwww.typescriptlang.org/docs/handbo…
this
When used as a type, this is dynamic, and it dynamically points to the type of the caller. The following two references are available:- www.typescriptlang.org/docs/handbo…
- www.typescriptlang.org/docs/handbo…
- Constructor, similar
new ()
Declare what type should be returned when creating an instance using new, but still support overloading- Refer to www.typescriptlang.org/docs/handbo…
- Type of “recursive” processing, for example
extend
,component
API handling of types - The use of various overloads is worth noting
CreateElement
The processing of - The basis of the Union Types www.typescriptlang.org/docs/handbo… As well as the Intersection computes Types www.typescriptlang.org/docs/handbo… The use of
InstanceType
As well astypeof
The use of
In addition to the above, we can also find some reloads to distinguish different components and Options, which are from the./ Options module, let’s look at the core implementation of the./ Options module, from the file github.com/vuejs/vue/b… :
import { Vue, CreateElement, CombinedVueInstance } from "./vue";
import { VNode, VNodeData, VNodeDirective, NormalizedScopedSlot } from "./vnode";
type Constructor = {
new(... args:any[]) :any;
}
// we don't support infer props in async component
// N.B. ComponentOptions<V> is contravariant, the default generic should be bottom type
/ / about the covariance and contravariance https://www.stephanboyer.com/post/132/what-are-covariance-and-contravariance
/ / https://jkchao.github.io/typescript-book-chinese/tips/covarianceAndContravariance.html in Chinese
export type Component<Data=DefaultData<never>, Methods=DefaultMethods<never>, Computed=DefaultComputed, Props=DefaultProps> =
| typeof Vue
| FunctionalComponentOptions<Props>
| ComponentOptions<never, Data, Methods, Computed, Props>
type EsModule<T> = T | { default: T }
type ImportedComponent<Data=DefaultData<never>, Methods=DefaultMethods<never>, Computed=DefaultComputed, Props=DefaultProps>
= EsModule<Component<Data, Methods, Computed, Props>>
export type AsyncComponent<Data=DefaultData<never>, Methods=DefaultMethods<never>, Computed=DefaultComputed, Props=DefaultProps>
= AsyncComponentPromise<Data, Methods, Computed, Props>
| AsyncComponentFactory<Data, Methods, Computed, Props>
export type AsyncComponentPromise<Data=DefaultData<never>, Methods=DefaultMethods<never>, Computed=DefaultComputed, Props=DefaultProps> = (
resolve: (component: Component<Data, Methods, Computed, Props>) => void, reject: (reason? :any) = >void
) = > Promise<ImportedComponent<Data, Methods, Computed, Props>> | void;
export type AsyncComponentFactory<Data=DefaultData<never>, Methods=DefaultMethods<never>, Computed=DefaultComputed, Props=DefaultProps> = () = > {
component: Promise<ImportedComponent<Data, Methods, Computed, Props>>; loading? : ImportedComponent; error? : ImportedComponent; delay? :number; timeout? :number;
}
/** * When the `Computed` type parameter on `ComponentOptions` is inferred, * it should have a property with the return type of every get-accessor. * Since there isn't a way to query for the return type of a function, we allow TypeScript * to infer from the shape of `Accessors
` and work backwards. */
export type Accessors<T> = {
/ / the use of https://www.typescriptlang.org/docs/handbook/2/mapped-types.html
[K in keyof T]: (() = > T[K]) | ComputedOptions<T[K]>
}
type DataDef<Data, Props, V> = Data | ((this: Readonly<Props> & V) = > Data)
// ThisTypedXxxx adds this type enhanced this context, About this type https://www.typescriptlang.org/docs/handbook/utility-types.html#thistypetype
/** * This type should be used when an array of strings is used for a component's `props` value. */
export type ThisTypedComponentOptionsWithArrayProps<V extends Vue, Data, Methods, Computed, PropNames extends string> =
object &
ComponentOptions<V, DataDef<Data, Record<PropNames, any>, V>, Methods, Computed, PropNames[], Record<PropNames, any>> &
ThisType<CombinedVueInstance<V, Data, Methods, Computed, Readonly<Record<PropNames, any> > > >;/** * This type should be used when an object mapped to `PropOptions` is used for a component's `props` value. */
export type ThisTypedComponentOptionsWithRecordProps<V extends Vue, Data, Methods, Computed, Props> =
object &
ComponentOptions<V, DataDef<Data, Props, V>, Methods, Computed, RecordPropsDefinition<Props>, Props> &
ThisType<CombinedVueInstance<V, Data, Methods, Computed, Readonly<Props>>>;
// Some default values
type DefaultData<V> = object | ((this: V) = > object);
type DefaultProps = Record<string.any>;
type DefaultMethods<V> = { [key: string] :(this: V, ... args:any[]) = > any };
type DefaultComputed = { [key: string] :any };
// Basic ComponentOptions Interface
export interface ComponentOptions<
V extends Vue,
Data=DefaultData<V>,
Methods=DefaultMethods<V>,
Computed=DefaultComputed,
PropsDef=PropsDefinition<DefaultProps>,
Props=DefaultProps> {
// Type inference automatically infer generic Data Methods, etcdata? : Data; props? : PropsDef; propsData? :object; computed? : Accessors<Computed>; methods? : Methods; watch? : Record<string, WatchOptionsWithHandler<any> | WatchHandler<any> >; el? : Element |string; template? :string;
// hack is for functional component type inference, should not be used in user coderender? (createElement: CreateElement,hack: RenderContext<Props>): VNode; renderError? (createElement: CreateElement,err: Error): VNode; staticRenderFns? : ((createElement: CreateElement) = >VNode)[]; beforeCreate? (this: V): void; created? () :void; beforeDestroy? () :void; destroyed? () :void; beforeMount? () :void; mounted? () :void; beforeUpdate? () :void; updated? () :void; activated? () :void; deactivated? () :void; errorCaptured? (err:Error.vm: Vue, info: string) :boolean | void; serverPrefetch? (this: V): Promise<void>; directives? : { [key:string]: DirectiveFunction | DirectiveOptions }; components? : { [key:string]: Component<any.any.any.any> | AsyncComponent<any.any.any.any> };
transitions?: { [key: string] :object}; filters? : { [key:string] :Function}; provide? :object | (() = > object); inject? : InjectOptions; model? : { prop? :string; event? :string; }; parent? : Vue; mixins? : (ComponentOptions<Vue> |typeofVue)[]; name? :string;
// TODO: support properly inferred 'extends'
extends? : ComponentOptions<Vue> |typeofVue; delimiters? : [string.string]; comments? :boolean; inheritAttrs? :boolean;
}
export interfaceFunctionalComponentOptions<Props = DefaultProps, PropDefs = PropsDefinition<Props>> { name? :string; props? : PropDefs; model? : { prop? :string; event? :string; }; inject? : InjectOptions; functional:boolean; render? (this: undefined.createElement: CreateElement, context: RenderContext<Props>): VNode | VNode[];
}
export interface RenderContext<Props=DefaultProps> {
props: Props;
children: VNode[];
slots(): any;
data: VNodeData;
parent: Vue;
listeners: { [key: string] :Function | Function[]}. scopedSlots: { [key:string]: NormalizedScopedSlot };
injections: any
}
// String => String
export type Prop<T> = { (): T } | { new(... args:never[]): T & object } | { new(... args:string[]) :Function }
export type PropType<T> = Prop<T> | Prop<T>[];
export type PropValidator<T> = PropOptions<T> | PropType<T>;
/ / PropOptions interface
export interface PropOptions<T=any> {
type? : PropType<T>; required? :boolean;
default? : T |null | undefined | (() = > T | null | undefined); validator? (value: T):boolean;
}
// Use keyof again to get the mapped Type
export type RecordPropsDefinition<T> = {
[K in keyof T]: PropValidator<T[K]>
}
export type ArrayPropsDefinition<T> = (keyof T)[];
export type PropsDefinition<T> = ArrayPropsDefinition<T> | RecordPropsDefinition<T>;
export interfaceComputedOptions<T> { get? (): T; set? (value: T):void; cache? :boolean;
}
export type WatchHandler<T> = string | ((val: T, oldVal: T) = > void);
export interfaceWatchOptions { deep? :boolean; immediate? :boolean;
}
export interface WatchOptionsWithHandler<T> extends WatchOptions {
handler: WatchHandler<T>;
}
export interface DirectiveBinding extends Readonly<VNodeDirective> {
readonly modifiers: { [key: string] :boolean };
}
export type DirectiveFunction = (el: HTMLElement, binding: DirectiveBinding, vnode: VNode, oldVnode: VNode) = > void;
export interfaceDirectiveOptions { bind? : DirectiveFunction; inserted? : DirectiveFunction; update? : DirectiveFunction; componentUpdated? : DirectiveFunction; unbind? : DirectiveFunction; }export type InjectKey = string | symbol;
export type InjectOptions = {
[key: string]: InjectKey | { from? : InjectKey,default? :any}} |string[];
Copy the code
You can see that there are some unique knowledge points involved here:
- For covariance and contravariance, there are some translations that correspond to covariance and contravariance
- www.stephanboyer.com/post/132/wh…
- Chinese jkchao. Making. IO/typescript -…
- Mapped Types www.typescriptlang.org/docs/handbo…
- About keyof www.typescriptlang.org/docs/handbo…
- About this type www.typescriptlang.org/docs/handbo…
- About never www.typescriptlang.org/docs/handbo…
/vue and./options are the two core parts of the vue./vue and./options are the two core parts of the vue.
extend
There will be overloading, and the core rationale for overloading isoptions
Different types of parameters: ThisTypedComponentOptionsWithArrayProps, ThisTypedComponentOptionsWithRecordProps, FunctionalComponentOptions, ComponentOptions is used as a downgrade case- The core of these differences are the different props, divided to the situation of the array, the Record (the object), FunctionalComponentOptions and relegation ComponentOptions situation
- Including FunctionalComponentOptions and divided to array and the object
- The core of type matching here is the use of TS’s type inference capabilities, which are also used in conjunction with function overload alignment
- After the type derivation is complete, the corresponding generic parameters, such as Props, Data, Methods, Computed, and so on, are obtained
- Based on the parameters derived above, use this type to enhance when written in the configuration item
this
The corresponding value can also be derived on the component instance
In this way, the type enhancement capability is realized. Here is only a partial view of the type enhancement implementation, all cases can be referred to the vUE official repository github.com/vuejs/vue/t… Module with enhanced tests for all types.
Type expansion
For users who want to extend Vue instance types or configuration items, the Vue official Type Enhancement document also provides details on how to do this. For details, see cn.vuejs.org/v2/guide/ty… As follows:
So what’s going on here? The core is on www.typescriptlang.org/docs/handbo… This property.
Is the basis of the TS statement provided by the ability to merge, detail you can refer to www.typescriptlang.org/docs/handbo… The document.
The simplified explanation is that the value of an interface can be merged in TS. If multiple modules declare the same interface, the value of the interface will be merged.
Related in-depth introduction, a supplement can refer to www.typescriptlang.org/docs/handbo…
Why
TS in the development of the whole community is beyond imagination, the vast majority of many of the framework, the library is the basic of TS development has been adopted, restricted by historical reasons, the Vue 2 is not realized by using TS (background reference especially big own answer www.zhihu.com/question/46.) .
Also, with the development of VS Code, built-in support for TS, or other IDE support for TS, you can have a great experience with TS.
The same is true for Vue users. People will have more and more demands for TS, and the support for TS type will come up.
Of course, we can also see that the cost of type support is quite high here, and due to the cost of TS, it is almost impossible to support it perfectly. However, Vue 3 includes the use of TS source Code, which makes a lot of special type enhancement, especially with the standardization and popularization of Composition API, and the support of VS Code related plug-ins, which can be said to be much better than Vue 2 in terms of type support. With a qualitative improvement, of course, you can also choose to use TSX and have a great experience.
conclusion
Overall Vue core support for type 2 this implementation, we have a general understanding and analysis, involves a lot of the characteristics of TS is also very much, most of the knowledge, we are in the process of the above analysis are also made corresponding some summary, these are need research, all the time and energy to learn.
With a general understanding of how TypeScript type declarations work as a provider, how developers should use them, and how to extend them.
Our team also had some related practices before, if interested students can refer to:
- BetterScroll 2.0 can be used at juejin.cn/post/689664… BetterScroll 2.0 can be used at juejin.cn/post/689664…
- In the support of Vue 3, we also contributed our own strength, for example, for
mixins
和extend
Inherit related type support, interested students can refer toGithub.com/vuejs/vue-n… - In general, for some of the things Vue 3 defineComponent does on type support, see juejin.cn/post/699461…
I hope I can help you.