Vue - The Good Parts: 类型支持

avatar
@滴滴出行

前言

虽然大家说 Vue 2 对于 TypeScript 支持不够好,用起来比较麻烦,但是这里边 Vue 也依旧做了很多事情,去做类型支持的能力。同时也官方也给出了另一个解法 github.com/vuejs/vue-c… ,当然 vue-class-component 不仅仅对于类型这块有了更好的支持,它最核心的是用类的方式写 Vue 组件。

今天我们这里更多的不是讨论 vue-class-component,而是重点关注与 Vue 2 本身对于类型支持这块所做的事情,一起来学习下对于一个并不是用 TS 写的项目,是怎么做类型支持的,对于我们自身的场景而言,有哪些可以借鉴的地方。

正文分析

What

类型支持其实就是对 TypeScript 的支持,虽然是片面的理解,但是也大概可以这么理解。那对于 TypeScript 的支持到底是个啥呢,做啥的?

这个首先就要认识 TypeScript,官网在这里 www.typescriptlang.org/,最核心的官方介绍(翻译过后的大概含义):TypeScript 就是带有类型语法的 JavaScript。TypeScript 给 JavaScript 提供了静态类型系统,可以真实有效地帮助我们防止很多运行时错误;此外,借助于 IDE 的支持,搭配静态类型系统,可以给到开发者很好的智能提示,“堆起”代码来速度快了不少。

那么 Vue 对于 TypeScript 的支持,核心就是要让 TypeScript 正确推断出 Vue 本身以及开发者定义的组件的类型,进而开发者使用的时候就可以获得对应的类型推断。详细的一些介绍文章可以参考文档 cn.vuejs.org/v2/guide/ty…

有了对 TypeScript 的支持之后,我们去写组件的时候,一个大概的效果应该是这样的:

image2021-9-7_14-49-53.png

可以看到,可以正确提醒 this 上的属性,如 type 这个 Prop,同时也会有原型上的 $nextTick 这样的方法的提醒。

How

那么这个支持是如何做的,一起看下 Vue 的处理。

增加类型入口声明

源文件在 github.com/vuejs/vue/b…。按照 TypeScript 规范,在 package.json 中新增typings的声明,指向类型声明文件。

类型声明文件

声明文件,github.com/vuejs/vue/b…,这里看后缀 .d.ts,这个在 TypeScript 中也是一个 Feature——类型声明文件,有详细的介绍 www.typescriptlang.org/docs/handbo…

借助于类型声明文件,即使没有相关 TS 声明的项目,我们也可以给这样的项目专门增加类型相关的声明,这样就达到了对于 TypeScript 类型支持的目的。最经典的项目就是 github.com/DefinitelyT…

那先来看下一下 index.d.ts 中的大概内容:

import { Vue } from "./vue";
import "./umd";
 
// 把 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";

一样做了模块的拆分,把各个模块暴露出来的 export 出去。这里我们重点分析下两个模块:./vue 模块和./options模块。

先看下最最最核心的 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 的类型,这里函数接口的重载
  // 区分了带有 data 的情况 还是没带 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 实例类型
export interface Vue {
  // 各种 API 的定义
  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[]>;
  // 支持链式调用 返回值 this
  $mount(elementOrSelector?: Element | string, hydrating?: boolean): this;
  $forceUpdate(): void;
  $destroy(): void;
  // 利用 typeof 直接获得 Vue.set 的类型
  $set: typeof Vue.set;
  $delete: typeof Vue.delete;
  // $watch 函数重载
  $watch(
    expOrFn: string,
    callback: (this: this, n: any, o: any) => void,
    options?: WatchOptions
  ): (() => void);
  // 类型自动匹配,获取泛型 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;
}
// 合并后的 Vue 实例,主要考虑传入的 options 的情况,需要把 data methods 等等的值 merge 到 this 上下文中
export type CombinedVueInstance<Instance extends Vue, Data, Methods, Computed, Props> =  Data & Methods & Computed & Props & Instance;
// Vue.extend 模式所最终得到的 Vue 构造器
export type ExtendedVue<Instance extends Vue, Data, Methods, Computed, Props> = VueConstructor<CombinedVueInstance<Instance, Data, Methods, Computed, Props> & Vue>;
 
// Vue.config 配置项 中可以配置的内容
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 构造器
export interface VueConstructor<V extends Vue = Vue> {
  // new 构造器的重载,这个是TS对于用 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, PropNames extends 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<PropNames extends 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<PropNames extends 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;
}
// 暴露出 Vue 值,这个值的类型就是 VueConstructor
export const Vue: VueConstructor;

我们先来看下这里边的一些知识点。

  • 两个 Vue 的迷惑,我们需要分清楚,上边的 interface Vue 声明的是一个类型,实际的含义是 Vue 实例,最后暴露出去的 const Vue 是声明暴露出去的一个值,这个值的类型是 VueConstructor 也就是 Vue 构造器。TS会自动根据你使用的场景来决定使用哪个含义
    • 首先你需要知道,在TS中存在类型 Types、值 Values、命名空间 Namespaces,他们的含义:www.typescriptlang.org/docs/handbo…
    • 在 TS 中是支持这种方式合并的,因为他们两个虽然同名,但是含义不同,相关文档 www.typescriptlang.org/docs/handbo…
    • 而且你还可以通过TS的文档知道,这个行为和你声明一个 class 的时候一样,class C 相对应的,在TS中会创建两个声明:一个叫 C 的类型,对应的就是 C 的实例,另一个是 C 的值,这个值就是指向的构造器函数。class 算是一种内置处理 www.typescriptlang.org/docs/handbo…
  • this 作为类型使用,此时的 this 是动态的,他会动态指向到调用方所对应的类型,一下两个相关介绍可以供参考:
  • 构造符,类似 new () 的方式声明当使用 new 创建实例的时候应该返回什么类型,依旧支持重载
  • 类型的“递归”处理,例如 extendcomponent API 对于类型的处理
  • 各种重载的使用,值得注意的是 CreateElement 的处理
  • 基础的 Union Types www.typescriptlang.org/docs/handbo… 以及 Intersection Types www.typescriptlang.org/docs/handbo… 的使用
  • InstanceType 以及 typeof 的使用

除了上述的,我们还可以发现一些重载的时候区分了不同的组件以及 Options,这些都是来自于./options模块,我们再来看下里边的核心实现,来自文件 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
// 关于 covariance 以及 contravariance https://www.stephanboyer.com/post/132/what-are-covariance-and-contravariance
// 中文的 https://jkchao.github.io/typescript-book-chinese/tips/covarianceAndContravariance.html
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<Computed>` and work backwards.
 */
export type Accessors<T> = {
  // 此处利用 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 加了 this type 增强的 this 上下文,关于 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>>>;
// 一些默认值
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 };
// 基础的 ComponentOptions interface
export interface ComponentOptions<
  V extends Vue,
  Data=DefaultData<V>,
  Methods=DefaultMethods<V>,
  Computed=DefaultComputed,
  PropsDef=PropsDefinition<DefaultProps>,
  Props=DefaultProps> {
  // 类型推断 自动推断出泛型 Data Methods 等
  data?: 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 code
  render?(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> | typeof Vue)[];
  name?: string;
  // TODO: support properly inferred 'extends'
  extends?: ComponentOptions<Vue> | typeof Vue;
  delimiters?: [string, string];
  comments?: boolean;
  inheritAttrs?: boolean;
}
 
export interface FunctionalComponentOptions<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 接口
export interface PropOptions<T=any> {
  type?: PropType<T>;
  required?: boolean;
  default?: T | null | undefined | (() => T | null | undefined);
  validator?(value: T): boolean;
}
 
// 再次利用 keyof 得到 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 interface ComputedOptions<T> {
  get?(): T;
  set?(value: T): void;
  cache?: boolean;
}
 
export type WatchHandler<T> = string | ((val: T, oldVal: T) => void);
 
export interface WatchOptions {
  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 interface DirectiveOptions {
  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[];

可以看到这里边有一些独特的知识点会涉及:


如果我们将核心的两部分./vue./options 两部分连起来就可以分析出来,Vue 里边是怎么做类型增强的,尤其是和 this 相关处理, 我们就以 extend 举例:

  • extend会存在重载,重载的核心依据就是options参数的不同类型:ThisTypedComponentOptionsWithArrayProps、ThisTypedComponentOptionsWithRecordProps、FunctionalComponentOptions,ComponentOptions 作为降级的情况下使用
    • 这几个的核心区别都在 props 的不同,划分为了 array 的情况、Record 的情况(即 对象)、FunctionalComponentOptions 以及降级的 ComponentOptions 的情况
    • 其中 FunctionalComponentOptions 又划分为了 array 和 对象的情况
  • 这里的类型匹配的核心是利用TS的类型推导能力,这个也是和函数重载对齐配合使用的
  • 类型推导完成之后,获得对应的泛型参数,如 Props、Data、Methods、Computed 等
  • 基于以上推导出来的参数,利用 this type 增强配置项中写的时候 this 的值,相对应的也可以得出组件实例上的值

这样就实现了类型增强的能力,这里也只是看了一部分的类型增强实现,所有的case可以参考 vue 官方仓库中的 github.com/vuejs/vue/t… 模块,里边有针对于所有的类型增强相关的测试。

类型扩充

对于使用者而言,如果想要扩充 Vue 实例类型或者配置项的话,Vue 官方类型增强文档中也给出具体的做法 ,详情可参考 cn.vuejs.org/v2/guide/ty… 大概如下:

image2021-9-9_17-4-29.png

那这里又是什么原理呢?核心就是靠 www.typescriptlang.org/docs/handbo… 这个特性。

这个的基础就是TS所能提供的声明合并能力,详细可以参考 www.typescriptlang.org/docs/handbo… 文档。

简化的解释就是,在 TS 中 interface 的值是可以合并的,如果多个模块对于同一个 interface 进行声明的话,这个 interface 的值是会合并到一起的。

相关深入的一个补充介绍,可以参考 www.typescriptlang.org/docs/handbo… 

Why

TS在整个社区的发展超出想象,绝大部分很多的框架、库基本都采用了TS开发,受限于历史原因,Vue 2 并没有采用 TS 实现(背景参考尤大自己的回答 www.zhihu.com/question/46…)。

同时随着 VS Code 的发展,对于 TS 的内置支持,或者其他 IDE 对于 TS 的支持,大家使用 TS 可以获得很好的体验。

那 Vue 的用户也是一样,大家对于 TS 的诉求会越来越多,对于 TS 类型这块的支持也就提上来了。

当然,我们也可以看到这里边做类型支持的成本还是挺高的,受限于 TS,受限于成本,想要支持的很完美基本不大可能。但是在 Vue 3 中包括利用 TS 实现源码,做了很多的特殊类型增强处理,尤其是也随着 Composition API 的标准化和普及,同时 VS Code 相关配套插件的支持,可以说在类型支持这块相比较于 Vue 2 强很多,有了一个质的提升,当然,你也可以选择使用 TSX,也可以获得很好的体验。

总结

整体 Vue 2 核心对于类型支持这块的实现,我们有了一个大概的了解和分析,涉及到的很多 TS 的特性也是非常之多的,大部分的知识点,我们在上述分析的过程中也都做了相应的一些总结,这些是需要大家花时间和精力研究一下,学习一下。

相信通过大概的了解,我们知道了作为一个提供者,怎么做 TypeScript 的类型声明,开发者应该如何使用,如何扩展。

我们团队之前也有一些相关的实践,如果有兴趣的同学可以参考:

  • BetterScroll 2.0 的类型推导文章中也有很多特性的应用的实战介绍,可以参考 juejin.cn/post/689664…
  • 在 Vue 3 这块的支持,我们这边也是贡献了自己的力量,例如,对于 mixinsextend 继承相关类型支持,感兴趣的同学可以参考 github.com/vuejs/vue-n…
  • 整体上关于 Vue 3 中的 defineComponent 相关的一些在类型上的支持所做的事情,可以参考 juejin.cn/post/699461…

希望可以对你有所帮助。