Author: Cui Jing

DefineComponent itself is simple, but the main function is to push to for types under TS. For a TS file, if we just write

export default {}
Copy the code

At this point, {} is just an Object type for the editor, so it doesn’t tell us what properties should be in {} for the vue component. But add a layer of defineComponet,

export default defineComponent({})
Copy the code

At this point, {} becomes the parameter of defineComponent, so the prompt for the parameter type can be realized for the attribute in {}, in addition to some type derivation of parameters and other operations.

But in the example above, if you try it in the.vue file of vscode, you will find that there is also a hint for not writing defineComponent. This is actually handled by the Vetur plugin.

Now let’s look at the implementation of defineComponent, there are four overloads, so let’s look at the simplest one, so I don’t care what defineComponent is, let’s look at it.

// overload 1: direct setup function
// (uses user defined props interface)
export function defineComponent<Props.RawBindings = object> (setup: ( props: Readonly
       
        , ctx: SetupContext ) => RawBindings | RenderFunction
       ) :DefineComponent<Props.RawBindings>
Copy the code

DefineComponet is function, function is props and CTX, return RawBindings or RenderFunction. DefineComponet returns type DefineComponent . There are two generic Props and RawBindings. Props will be determined by the type we pass in to setup as the first parameter when we actually write it, and RawBindings will be determined by the value we return to setup. A long paragraph is a bit convoluted, so write a simple example like this: ,>

  • A simple demo like props looks like this. We pass different types of parameters to a and return different types of define. These are called Generic Functions

    declare function define<Props> (a: Props) :Props
    
    const arg1:string = '123'
    const result1 = define(arg1) / /result1:string
    
    const arg2:number = 1
    const result2 = define(arg2) / /result2: number
    Copy the code
  • A simple demo like RawBindings is as follows: Setup returns a different type, define returns a different type

    declare function define<T> (setup: ()=>T) :T
    
    const arg1:string = '123'
    const resul1 = define(() = > {return arg1
    })
    
    const arg2:number = 1
    const result2 = define(() = > {return arg2
    })
    Copy the code

DefineComponet (Props, RawBindings> Props); defineComponet (Props, RawBindings>); RawBindings is a return value type for setup. If we return a function, take the default value object. From this you can get a basic usage of ts derivation for the following definition

declare function define<T> (a: T) :T
Copy the code

You can dynamically determine the type of T based on the arguments that are passed in at runtime. This is the only way that runtime types are related to typescript static types. We can use this for many times when we want to determine other related types by the arguments that are passed in at runtime.

Moving on to definComponent, it overloads 2, 3, and 4 to handle the different types of props in options. Look at the most common props declaration for type object

export function defineComponent<
  // the Readonly constraint allows TS to treat the type of { required: true }
  // as constant instead of boolean.
  PropsOptions extends Readonly<ComponentPropsOptions>,
  RawBindings,
  D,
  C extends ComputedOptions = {},
  M extends MethodOptions = {},
  Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
  Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
  E extends EmitsOptions = Record<string.any>,
  EE extends string = string
>(
  options: ComponentOptionsWithObjectProps<
    PropsOptions,
    RawBindings,
    D,
    C,
    M,
    Mixin,
    Extends,
    E,
    EE
  >
): DefineComponent<PropsOptions, RawBindings, D, C, M, Mixin, Extends, E, EE>
Copy the code

Similar to the idea of overloading 1 above, the core idea is to derive generics based on what options are written at runtime. The first parameter to setup in vue3 is props. This props must have the same type as the one we passed in options. This is in ComponentOptionsWithObjectProps implementation. The following code

export type ComponentOptionsWithObjectProps<
  PropsOptions = ComponentObjectPropsOptions,
  RawBindings = {},
  D = {},
  C extends ComputedOptions = {},
  M extends MethodOptions = {},
  Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
  Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
  E extends EmitsOptions = EmitsOptions,
  EE extends string = string,
  Props = Readonly<ExtractPropTypes<PropsOptions>>,
  Defaults = ExtractDefaultPropTypes<PropsOptions>
> = ComponentOptionsBase<
  Props,
  RawBindings,
  D,
  C,
  M,
  Mixin,
  Extends,
  E,
  EE,
  Defaults
> & {
  props: PropsOptions & ThisType<void>
} & ThisType<
    CreateComponentPublicInstance<
      Props,
      RawBindings,
      D,
      C,
      M,
      Mixin,
      Extends,
      E,
      Props,
      Defaults,
      false
    >
  >
    
export interface ComponentOptionsBase<
  Props,
  RawBindings,
  D,
  C extends ComputedOptions,
  M extends MethodOptions,
  Mixin extends ComponentOptionsMixin,
  Extends extends ComponentOptionsMixin,
  E extends EmitsOptions,
  EE extends string = string,
  Defaults = {}
>
  extendsLegacyOptions<Props, D, C, M, Mixin, Extends>, ComponentInternalOptions, ComponentCustomOptions { setup? :(
        this: void,
        props: Props,
        ctx: SetupContext<E, Props>
      ) = > Promise<RawBindings> | RawBindings | RenderFunction | void
    / /...
  }
Copy the code

It’s a long one, but let’s do the same thing with a simplified demo:

type TypeA<T1, T2, T3> = {
  a: T1,
  b: T2,
  c: T3
}
declare function define<T1.T2.T3> (options: TypeA<T1, T2, T3>) :T1
const result = define({
  a: '1',
  b: 1,
  c: {}
}) / /result: string
Copy the code

The types of T1,T2, and T3 are inferred from the options parameter ts passed in. Once you have T1, T2, and T3, you can use them to make other inferences. To modify the demo slightly, assume that c is a function whose argument types are determined by the type of a:

type TypeA<T1, T2, T3> = TypeB<T1, T2>
type TypeB<T1, T2> = {
  a: T1
  b: T2,
  c: (arg:T1) = >{}}const result = define({
  a: '1'.b: 1.c: (arg) = > {  // arg is derived as a string
    return arg
  }
})
Copy the code

Then look at the code in Vue. First defineComponent can derive PropsOptions. But if props were an object type, it would look like this

props: { name: { type: String, //... Other attributes}}Copy the code

For the props argument in setup, you need to extract the type type from it. So in the ComponentOptionsWithObjectProps

export type ComponentOptionsWithObjectProps<
  PropsOptions = ComponentObjectPropsOptions,
  / /...
  Props = Readonly<ExtractPropTypes<PropsOptions>>,
  / /...
>
Copy the code

PropsOptions is transformed via ExtracPropTypes, and then Props are passed in to ComponentOptionsBase, which is used as the type of the setup parameter

export interface ComponentOptionsBase<
  Props,
  //...
>
  extendsLegacyOptions<Props, D, C, M, Mixin, Extends>, ComponentInternalOptions, ComponentCustomOptions { setup? :(
    this: void,
    props: Props,
    ctx: SetupContext<E, Props>
  ) = > Promise<RawBindings> | RawBindings | RenderFunction | void
Copy the code

This implements the derivation of props.

  • The effect of this

    The first one in the setup definition is this:void. When we write logic in the setup function, we will see an error message in the IDE if this. XXX is used

    Property ‘xxx’ does not exist on type ‘void’

    This avoids using this in setup by setting this:void.

    This is a special case in JS because it depends on the context in which it is run. Typescript sometimes fails to deduce exactly what type this is used in our code, so this becomes any. (Note: ts will only derive the type of this if noImplicitThis is enabled). To solve this problem, typescript functions can explicitly write a this argument, such as in the official website example:

    interface Card {
      suit: string;
      card: number;
    }
    
    interface Deck {
      suits: string[];
      cards: number[];
      createCardPicker(this: Deck): () = > Card;
    }
    
    let deck: Deck = {
      suits: ["hearts"."spades"."clubs"."diamonds"].cards: Array(52),
      // NOTE: The function now explicitly specifies that its callee must be of type Deck
      createCardPicker: function (this: Deck) {
        return () = > {
          let pickedCard = Math.floor(Math.random() * 52);
          let pickedSuit = Math.floor(pickedCard / 13);
    
          return { suit: this.suits[pickedSuit], card: pickedCard % 13}; }; }};let cardPicker = deck.createCardPicker();
    let pickedCard = cardPicker();
    
    alert("card: " + pickedCard.card + " of " + pickedCard.suit);
    Copy the code

    It explicitly defines that this in the createCardPicker is the type of Deck. So the properties/methods that are available under this in the createCardPicker are restricted to Deck.

    In addition to this, there is a ThisType.

ExtractPropTypes and ExtractDefaultPropTypes

It mentioned the props we wrote

{
  props: {
    name1: {
      type: String,
      require: true
    },
    name2: {
      type: Number
    }
  }
}
Copy the code

After derivation by defineComponent, it is converted to the type of TS

ReadOnly<{ name1: string, name2? : number | undefined }>Copy the code

This process is implemented using ExtractPropTypes.

export type ExtractPropTypes<O> = O extends object
  ? { [K in RequiredKeys<O>]: InferPropType<O[K]> } &
      { [K inOptionalKeys<O>]? : InferPropType<O[K]> } : { [Kin string] :any }
Copy the code

The clear naming in the type makes it easy to understand: use RrequiredKeys

and OptionsKeys

to split O according to whether it is required (using the previous props example)

{name1} & {name2? }Copy the code

Then in each set, derive the type from InferPropType

.
[k]>

  • InferPropType

    So before we understand that, let’s just understand some simple derivations. First of all, let’s write it in code

    props = {
      type: String
    }
    Copy the code

    Props. Type is of type StringConstructor. So the first step is to get the corresponding type string/number from StringConstructor/ NumberConstructor etc. This can be done by infer

    type a = StringConstructor
    type ConstructorToType<T> = T extends  { (): infer V } ? V : never
    type c = ConstructorToType<a> // type c = String
    Copy the code

    Above, we can infer the type by ():infer V. The reason why this is possible is related to the implementation of types such as String/Number. You can write it in javascript

    const key = String('a')
    Copy the code

    In this case, key is of type string. You can also look at the StringConstructor interface type representation

    interface StringConstructor {
        new(value? :any) :String; (value? :any) :string;
        readonly prototype: String; fromCharCode(... codes:number[]) :string;
    }
    Copy the code

    There’s a ():string above, so the “V” inferred from extends {(): infer V} is a string.

    Then, modify the above “A” into the contents in propsOptions, and infer V in the ConstructorToType to the outer layer for judgment

    type a = StringConstructor
    type ConstructorType<T> = { (): T }
    type b = a extends {
      type: ConstructorType<infer V> required? :boolean}? V :never // type b = String
    Copy the code

    This is a simple implementation of converting the contents of props to the types in type.

    The actual code implementation in VUe3 is a little more complicated because the props type supports a lot of Chinese writing

    type InferPropType<T> = T extends null
      ? any // null & true would fail to infer
      : T extends { type: null | true}?any 
        // As TS issue https://github.com/Microsoft/TypeScript/issues/14829 // somehow `ObjectConstructor` when inferred from { (): T } becomes `any` // `BooleanConstructor` when inferred from PropConstructor(with PropMethod) becomes `Boolean`
        // ObjectConstructor and BooleanConstructor are judged separately
        : T extends ObjectConstructor | { type: ObjectConstructor }
          ? Record<string.any>
          : T extends BooleanConstructor | { type: BooleanConstructor }
            ? boolean
            : T extends Prop<infer V, infer D> ? (unknown extends V ? D : V) : T
    
    // Both PropOptions and PropType are supported
    type Prop<T, D = T> = PropOptions<T, D> | PropType<T>
    interface PropOptions<T = any, D = T> {
      type? : PropType<T> |true | nullrequired? :boolean
      default? : D | DefaultFactory<D> |null | undefined | objectvalidator? (value: unknown):boolean
    }
    
    export type PropType<T> = PropConstructor<T> | PropConstructor<T>[]
    
    type PropConstructor<T = any> =
      | { new(... args:any[]): T & object } // This can be matched with other Constructor
      | { (): T }  / / can match to the StringConstructor/NumberConstructor and () = > string, etc
      | PropMethod<T> Type: (a: number, b: string) => string
    
    // For the form Function, construct a type and stringConstructor using PropMethod
    // PropMethod is one of the PropType types
    // When we write type: Function as PropType<(a: string) => {b: string}>, this is converted to
    // type: (new (... args: any[]) => ((a: number, b: string) => {
    // a: boolean;
    // }) & object) | (() => (a: number, b: string) => {
    // a: boolean;
    / / | {})
    // (): (a: number, b: string) => {
    // a: boolean;
    / /};
    // new (): any;
    // readonly prototype: any;
    // }
    (a:number,b:string) => {a: Boolean}
    type PropMethod<T, TConstructor = any> = 
      T extends(... args:any) = >any // if is function with args
      ? { 
          new (): TConstructor; 
          (): T; 
          readonly prototype: TConstructor 
        } // Create Function like constructor
      : never
    Copy the code
  • RequiredKeys

    This is used to separate the props from the props that must have a value

    type RequiredKeys<T> = {
      [K in keyof T]: T[K] extends
        | { required: true }
        | { default: any }
        // don't mark Boolean props as undefined
        | BooleanConstructor
        | { type: BooleanConstructor }
        ? K
        : never
    }[keyof T]
    Copy the code

    In addition to explicitly defining reqruied, it also contains a default value, or a Boolean type. Because for Boolean if we don’t pass it in, it defaults to false; The default value of prop, must not be undefined

  • OptionalKeys

    With RequiredKeys, OptionsKeys are simple: exclude the RequiredKeys

    type OptionalKeys<T> = Exclude<keyof T, RequiredKeys<T>>
    Copy the code

ExtractDefaultPropTypes are similar to ExtractPropTypes, so I won’t write them.

For method, computed, and data in options, the results are similar to those for props.

emits options

Options in vue3 has a new emits configuration that displays the events we want to dispatch in the component. The event configured in emits will be prompted as the first argument to the function when we write $emit.

The method used to get the configuration value in emits is similar to the method used to get the type in props. $Emit prompts are implemented through ThisType (see another article on ThisType). Here’s a simplified demo

declare function define<T> (props:{ emits: T, method? : {[key:string] : (... arg:any) = >any}
} & ThisType<{
  $emits: (arg: T) => void} >) :T

const result = define({
  emits: {
    key: '123'
  },
  method: {
    fn() {
      this$emits(/*) arg: {key:string; }}}} * /))
Copy the code

It follows that T is the type in emits. Then & ThisType makes it possible to use this.$emit in the method. $emit = “this.$emit” = “this.$emit” = “this.

Then look at the implementation in VuE3

export function defineComponent< / /... Save the restE extends EmitsOptions = Record<string.any> / /... > (
  options: ComponentOptionsWithObjectProps<
    / /...
    E,
    / /...
  >
) :DefineComponent<PropsOptions.RawBindings.D.C.M.Mixin.Extends.E.EE>

export type ComponentOptionsWithObjectProps< / /..E extends EmitsOptions = EmitsOptions, / /... > =ComponentOptionsBase< // Define oneEThe generic / /...E, / /... > &{
  props: PropsOptions & ThisType<void>
} & ThisType<
    CreateComponentPublicInstance<  // Use ThisType to implement the prompt in $Emit
      / /...
      E,
      / /...
    >
  >
    
/ / ComponentOptionsWithObjectProps includes ComponentOptionsBase
export interface ComponentOptionsBase<
  //...
  E extendsEmitsOptions, // type EmitsOptions = Record<string, ((... args: any[]) => any) | null> | string[] EEextends string = string,
  Defaults = {}
>
  extends LegacyOptions<Props, D, C, M, Mixin, Extends>,
    ComponentInternalOptions,
    ComponentCustomOptions {
      / /..emits? : (E | EE[]) & ThisType<void>  // Deduce the type of E
}
      
export type ComponentPublicInstance<
  / /...
  E extends EmitsOptions = {},
  / /...
> = {
  / /...
  $emit: EmitFn<E>  // EmitFn to extract the key in E
  / /...
}
Copy the code

Step on a pothole while learning and practicing. Step pit process: the derivation process of emits is realized

export type ObjectEmitsOptions = Record<
  string,
  ((. args:any[]) = > any) | null
>
export type EmitsOptions = ObjectEmitsOptions | string[];

declare function define<E extends EmitsOptions = Record<string.any>, EE extends string = string> (options: E| EE[]) : (E | EE[]) & ThisType<void>
Copy the code

Then verify the results in the following way

const emit = ['key1', 'key2']
const a = define(emit)
Copy the code

A = const b: string[] &thistype

((“key1” | “key2”)[] & ThisType

) | ((“key1” | “key2”)[] & ThisType

)

Struggle for a long time, and finally found the different writing method: using the following writing method derived the result of the same

define(['key1', 'key2'])
Copy the code

When ts gets the emit, its type is derived to string[], so the define function gets string[] instead of the original [‘key1’, ‘key2’].

Note: when defining emits in vue3, it is recommended to write it directly in emits instead of extracting it as a separate variable and passing it to emits

If it does, it needs to be processed so that the variable definition of ‘[key1’, ‘key2’] returns type [‘key1’, ‘key2’] and not string[]. You can do this in the following two ways:

  • Methods a

    const keys = ["key1"."key2"] as const; // const keys: readonly ["key1", "key2"]
    Copy the code

    It’s easier to write this way. However, there is a drawback: keys are converted to readonly, so you cannot modify keys later.

    Ways to create a Union from an Array in Typescript

  • Way 2

    type UnionToIntersection<T> = (T extends any ? (v: T) = > void : never) extends (v: infer V) => void ? V : never
    type LastOf<T> = UnionToIntersection<T extends any ? () = > T : never> extends () => infer R ? R : never
    type Push<T extends any[], V> = [ ...T, V]
    
    type UnionToTuple<T, L = LastOf<T>, N = [T] extends [never]?true : false> = N extends true ? [] : Push<UnionToTuple<Exclude<T, L>>, L>
    
    declare function tuple<T extends string> (arr: T[]) :UnionToTuple<T>
    
    const c = tuple(['key1'.'key2']) / /const c: ["key1","key2"]
    Copy the code

    First, [‘key1’, ‘key2’] is converted to a union by arr: T[], and then, recursively, LastOf gets the LastOf the union and pushes it into the array.

Mixins and extends

Content written in mixins or extends in VuE3 can be prompted in this. For mixins and extends, there’s one big difference from the other types of inference above: recursion. So when it comes to type determination, it also needs recursive processing. Here’s a simple example

const AComp = {
  methods: {
    someA(){}
  }
}
const BComp = {
  mixins: [AComp],
  methods: {
    someB() {}
  }
}
const CComp = {
  mixins: [BComp],
  methods: {
    someC() {}
  }
}
Copy the code

For the hint of this in CComp, there should be methods someB and someA. To do this, you need a ThisType similar to the following when doing type inference

ThisType<{
  someA
} & {
  someB
} & {
  someC
}>
Copy the code

So to handle mixins, you need to recursively retrieve the contents of the Mixins in the Component, and then convert the nested types to flat, linked by &. Look at the source code implementation:

// Determine whether there are mixins in T
{mixin: any} {mixin: any} {mixin? : any} cannot extend each other
type IsDefaultMixinComponent<T> = T extends ComponentOptionsMixin
  ? ComponentOptionsMixin extends T ? true : false
  : false

// 
type IntersectionMixin<T> = IsDefaultMixinComponent<T> extends true
  ? OptionTypesType<{}, {}, {}, {}, {}>  // T does not contain mixins, so the recursion ends and returns {}
  : UnionToIntersection<ExtractMixin<T>> // Get the contents of the Mixin in T for recursion

// ExtractMixin(map type) is used to resolve circularly references
type ExtractMixin<T> = {
  Mixin: MixinToOptionTypes<T>
}[T extends ComponentOptionsMixin ? 'Mixin' : never]

// infer from T to obtain the Mixin, and then recursively call IntersectionMixin
      
       .
      
type MixinToOptionTypes<T> = T extends ComponentOptionsBase<
  infer P,
  infer B,
  infer D,
  infer C,
  infer M,
  infer Mixin,
  infer Extends,
  any.any,
  infer Defaults
>
  ? OptionTypesType<P & {}, B & {}, D & {}, C & {}, M & {}, Defaults & {}> &
      IntersectionMixin<Mixin> &
      IntersectionMixin<Extends>
  : never
Copy the code

The process for extends is the same as for mixins. Then look at the processing in ThisType

ThisType<
    CreateComponentPublicInstance<
      Props,
      RawBindings,
      D,
      C,
      M,
      Mixin,
      Extends,
      E,
      Props,
      Defaults,
      false
    >
  >
export type CreateComponentPublicInstance<
  P = {},
  B = {},
  D = {},
  C extends ComputedOptions = {},
  M extends MethodOptions = {},
  Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
  Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
  E extends EmitsOptions = {},
  PublicProps = P,
  Defaults = {},
  MakeDefaultsOptional extends boolean = false.// Convert a nested structure to a flat one
  PublicMixin = IntersectionMixin<Mixin> & IntersectionMixin<Extends>,
  / / extraction props
  PublicP = UnwrapMixinsType<PublicMixin, 'P'> & EnsureNonVoid<P>,
  // Extract RawBindings, which are returned by setup
  PublicB = UnwrapMixinsType<PublicMixin, 'B'> & EnsureNonVoid<B>,
  // Extract the content returned by data
  PublicD = UnwrapMixinsType<PublicMixin, 'D'> & EnsureNonVoid<D>,
  PublicC extends ComputedOptions = UnwrapMixinsType<PublicMixin, 'C'> &
    EnsureNonVoid<C>,
  PublicM extends MethodOptions = UnwrapMixinsType<PublicMixin, 'M'> &
    EnsureNonVoid<M>,
  PublicDefaults = UnwrapMixinsType<PublicMixin, 'Defaults'> &
    EnsureNonVoid<Defaults>
> = ComponentPublicInstance< // Pass the above result to ComponentPublicInstance to generate the contents of this context
  PublicP,
  PublicB,
  PublicD,
  PublicC,
  PublicM,
  E,
  PublicProps,
  PublicDefaults,
  MakeDefaultsOptional,
  ComponentOptionsBase<P, B, D, C, M, Mixin, Extends, E, string, Defaults>
>
Copy the code

Above is the overall majority of defineComponent implementation, as you can see, it is purely for type derivation, at the same time, there are many, many types of derivation techniques used here, there are some not covered here, interested students can take a look at the implementation in Vue.