Skip to content
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

ComponentCustomProps is erased in distributed component types #8376

Closed
KaelWD opened this issue May 19, 2023 · 8 comments · Fixed by #9468
Closed

ComponentCustomProps is erased in distributed component types #8376

KaelWD opened this issue May 19, 2023 · 8 comments · Fixed by #9468
Labels
❗ p4-important Priority 4: this fixes bugs that violate documented behavior, or significantly improves perf. regression scope: types

Comments

@KaelWD
Copy link
Contributor

KaelWD commented May 19, 2023

Vue version

3.3.2

Link to minimal reproduction

vuetifyjs/vuetify#16190

Steps to reproduce

Build a library with

DefineComponent<MyProps, {}, {}, {}, {}, {}, {}, {}, {}, VNodeProps & AllowedComponentProps & ComponentCustomProps>

What is expected?

The emitted type includes

$props: MyProps & vue.VNodeProps & vue.AllowedComponentProps & vue.ComponentCustomProps

Consumers of the library can use module augmentation to set ComponentCustomProps

What is actually happening?

Prettify added in 4c9bfd2 flattens it to

$props: { foo?: string, bar?: string }

Consumers cannot add anything with ComponentCustomProps

System Info

No response

Any additional comments?

vuetifyjs/vuetify#16190 (comment)

@ml1nk
Copy link

ml1nk commented May 19, 2023

I was creating an issue myself, but you were faster. Thanks.

https://stackblitz.com/edit/vitejs-vite-uq3thn?file=src/App.vue

❯ npm run typecheck
$ vue-tsc --noEmit
src/App.vue:7:10 - error TS2345: Argument of type '{ abcd: string; label: string; }' is not assignable to parameter of type '{ symbol?: any; replace?: boolean | undefined; flat?: boolean | undefined; exact?: boolean | undefined; block?: boolean | undefined; active?: boolean | undefined; style?: StyleValue | undefined; ... 47 more ...; "v-slot:loader"?: false | ... 1 more ... | undefined; }'.
  Object literal may only specify known properties, and 'abcd' does not exist in type '{ symbol?: any; replace?: boolean | undefined; flat?: boolean | undefined; exact?: boolean | undefined; block?: boolean | undefined; active?: boolean | undefined; style?: StyleValue | undefined; ... 47 more ...; "v-slot:loader"?: false | ... 1 more ... | undefined; }'.

7   <v-btn abcd="string" label="test" />
           ~~~~~~~~~~~~~


Found 1 error in src/App.vue:7

App.vue

<script lang="ts" setup>
import HelloWorld from './components/HelloWorld.vue';
</script>

<template>
  <HelloWorld msg="Vite + Vue" abcd="string" />
  <v-btn abcd="string" label="test" />
</template>

shim.d.ts

declare module 'vue' {
  interface ComponentCustomProps {
    abcd: string;
  }
}

export {};

@KaelWD
Copy link
Contributor Author

KaelWD commented Jun 14, 2023

@shengxinjing No difference with 5.1.3

@pikax
Copy link
Member

pikax commented Oct 13, 2023

Can't reproduce this locally, even when copying most of defineComponent.tsx types and build them, they still get generated as expected with vue-tsc and tsc, I suspect the bad typing is caused by a misconfiguration on vuetify instead of being caused by vue

here's my test
main.ts

// Types
import type {
  AllowedComponentProps,
  ComponentCustomProps,
  ComponentInjectOptions,
  ComponentObjectPropsOptions,
  ComponentOptions,
  ComponentOptionsMixin,
  ComponentOptionsWithObjectProps,
  ComponentOptionsWithoutProps,
  ComponentPropsOptions,
  ComputedOptions,
  DefineComponent,
  EmitsOptions,
  ExtractDefaultPropTypes,
  ExtractPropTypes,
  FunctionalComponent,
  MethodOptions,
  ObjectEmitsOptions,
  SlotsType,
  VNode,
  VNodeChild,
  VNodeProps,
} from "vue";

export declare function betterDefineComponent<P>(p: P): DefineComponent<P>;

// vuetify.d.ts

export type SlotsToProps<U extends RawSlots, T = MakeInternalSlots<U>> = {
  $children?:
    | VNodeChild
    | (T extends { default: infer V } ? V : {})
    | { [K in keyof T]?: T[K] };
  "v-slots"?: { [K in keyof T]?: T[K] | false };
} & {
  [K in keyof T as `v-slot:${K & string}`]?: T[K] | false;
};

type RawSlots = Record<string, unknown>;
type Slot<T> = [T] extends [never] ? () => VNodeChild : (arg: T) => VNodeChild;
type VueSlot<T> = [T] extends [never] ? () => VNode[] : (arg: T) => VNode[];
type MakeInternalSlots<T extends RawSlots> = {
  [K in keyof T]: Slot<T[K]>;
};
type MakeSlots<T extends RawSlots> = {
  [K in keyof T]: VueSlot<T[K]>;
};

export type GenericProps<Props, Slots extends Record<string, unknown>> = {
  $props: Props & SlotsToProps<Slots>;
  $slots: MakeSlots<Slots>;
};

type ToListeners<T extends string | number | symbol> = {
  [K in T]: K extends `on${infer U}` ? Uncapitalize<U> : K;
}[T];

type EmitsToProps<T extends EmitsOptions> = T extends string[]
  ? {
      [K in string & `on${Capitalize<T[number]>}`]?: (...args: any[]) => any;
    }
  : T extends ObjectEmitsOptions
  ? {
      [K in string &
        `on${Capitalize<string & keyof T>}`]?: K extends `on${infer C}`
        ? T[Uncapitalize<C>] extends null
          ? (...args: any[]) => any
          : (
              ...args: T[Uncapitalize<C>] extends (...args: infer P) => any
                ? P
                : never
            ) => any
        : never;
    }
  : {};

type PublicProps = VNodeProps & AllowedComponentProps & ComponentCustomProps;

// Adds a filterProps method to the component options
export interface FilterPropsOptions<
  PropsOptions extends Readonly<ComponentPropsOptions>,
  Props = ExtractPropTypes<PropsOptions>
> {
  filterProps<
    T extends Partial<Props>,
    U extends Exclude<keyof Props, Exclude<keyof Props, keyof T>>
  >(
    props: T
  ): [yes: Partial<Pick<T, U>>, no: Omit<T, U>];
}

type DefineComponentWithGenericProps<
  T extends new (props: Record<string, any>, slots: RawSlots) => {
    $props?: Record<string, any>;
  }
> = <
  PropsOptions extends Readonly<ComponentObjectPropsOptions>,
  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,
  I extends ComponentInjectOptions = {},
  II extends string = string,
  // Slots extends RawSlots = ConstructorParameters<T> extends [any, infer SS extends RawSlots | undefined] ? Exclude<SS, undefined> : {},
  Slots extends RawSlots = ConstructorParameters<T>[1],
  S extends SlotsType = SlotsType<Partial<MakeSlots<Slots>>>,
  III = InstanceType<T>,
  P = III extends Record<"$props", any>
    ? Omit<PropsOptions, keyof III["$props"]>
    : PropsOptions,
  Base = DefineComponent<
    P,
    RawBindings,
    D,
    C,
    M,
    Mixin,
    Extends,
    E extends any[]
      ? E
      : III extends Record<"$props", any>
      ? Omit<E, ToListeners<keyof III["$props"]>>
      : E,
    EE,
    PublicProps,
    ExtractPropTypes<P> & ({} extends E ? {} : EmitsToProps<E>),
    ExtractDefaultPropTypes<P>,
    S
  >
>(
  options: ComponentOptionsWithObjectProps<
    PropsOptions,
    RawBindings,
    D,
    C,
    M,
    Mixin,
    Extends,
    E,
    EE,
    I,
    II,
    S
  >
) => Base & T & FilterPropsOptions<PropsOptions>;

type DefineComponentWithSlots<Slots extends RawSlots> = <
  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,
  I extends ComponentInjectOptions = {},
  II extends string = string,
  S extends SlotsType = SlotsType<Partial<MakeSlots<Slots>>>
>(
  options: ComponentOptionsWithObjectProps<
    PropsOptions,
    RawBindings,
    D,
    C,
    M,
    Mixin,
    Extends,
    E,
    EE,
    I,
    II,
    S
  >
) => DefineComponent<
  ExtractPropTypes<PropsOptions> & SlotsToProps<Slots>,
  RawBindings,
  D,
  C,
  M,
  Mixin,
  Extends,
  E,
  EE,
  PublicProps,
  ExtractPropTypes<PropsOptions> &
    SlotsToProps<Slots> &
    ({} extends E ? {} : EmitsToProps<E>),
  ExtractDefaultPropTypes<PropsOptions>,
  S
> &
  FilterPropsOptions<PropsOptions>;

// No argument - simple default slot
export function genericComponent(
  exposeDefaults?: boolean
): DefineComponentWithSlots<{ default: never }>;

// Generic constructor argument - generic props and slots
export function genericComponent<
  T extends new (props: Record<string, any>, slots: any) => {
    $props?: Record<string, any>;
  }
>(exposeDefaults?: boolean): DefineComponentWithGenericProps<T>;

// Slots argument - simple slots
export function genericComponent<Slots extends RawSlots>(
  exposeDefaults?: boolean
): DefineComponentWithSlots<Slots>;

// Implementation
export function genericComponent(exposeDefaults = true) {
  return (options: any) =>
    ((exposeDefaults ? defineComponent : _defineComponent) as any)(options);
}

export const Comp = genericComponent();

export type VBtnSlots = {
  default: never;
  prepend: never;
  append: never;
  loader: never;
};
export const VBtn = genericComponent<VBtnSlots>();

tsconfig - basically default vite + outDir and declaration

{
  "compilerOptions": {
    "target": "ES2020",
    "outDir": "dist",
    "declaration": true,
    "useDefineForClassFields": true,
    "module": "ESNext",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "skipLibCheck": true,

    /* Bundler mode */
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "isolatedModules": true,

    /* Linting */
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true
  },
  "include": ["src"]
}
pnpm tsc --emitDeclarationOnly
pnpm vue-tsc --emitDeclarationOnly

built file

import type { AllowedComponentProps, ComponentCustomProps, ComponentInjectOptions, ComponentObjectPropsOptions, ComponentOptionsMixin, ComponentOptionsWithObjectProps, ComponentPropsOptions, ComputedOptions, DefineComponent, EmitsOptions, ExtractDefaultPropTypes, ExtractPropTypes, MethodOptions, ObjectEmitsOptions, SlotsType, VNode, VNodeChild, VNodeProps } from "vue";
export declare function betterDefineComponent<P>(p: P): DefineComponent<P>;
export type SlotsToProps<U extends RawSlots, T = MakeInternalSlots<U>> = {
    $children?: VNodeChild | (T extends {
        default: infer V;
    } ? V : {}) | {
        [K in keyof T]?: T[K];
    };
    "v-slots"?: {
        [K in keyof T]?: T[K] | false;
    };
} & {
    [K in keyof T as `v-slot:${K & string}`]?: T[K] | false;
};
type RawSlots = Record<string, unknown>;
type Slot<T> = [T] extends [never] ? () => VNodeChild : (arg: T) => VNodeChild;
type VueSlot<T> = [T] extends [never] ? () => VNode[] : (arg: T) => VNode[];
type MakeInternalSlots<T extends RawSlots> = {
    [K in keyof T]: Slot<T[K]>;
};
type MakeSlots<T extends RawSlots> = {
    [K in keyof T]: VueSlot<T[K]>;
};
export type GenericProps<Props, Slots extends Record<string, unknown>> = {
    $props: Props & SlotsToProps<Slots>;
    $slots: MakeSlots<Slots>;
};
type ToListeners<T extends string | number | symbol> = {
    [K in T]: K extends `on${infer U}` ? Uncapitalize<U> : K;
}[T];
type EmitsToProps<T extends EmitsOptions> = T extends string[] ? {
    [K in string & `on${Capitalize<T[number]>}`]?: (...args: any[]) => any;
} : T extends ObjectEmitsOptions ? {
    [K in string & `on${Capitalize<string & keyof T>}`]?: K extends `on${infer C}` ? T[Uncapitalize<C>] extends null ? (...args: any[]) => any : (...args: T[Uncapitalize<C>] extends (...args: infer P) => any ? P : never) => any : never;
} : {};
type PublicProps = VNodeProps & AllowedComponentProps & ComponentCustomProps;
export interface FilterPropsOptions<PropsOptions extends Readonly<ComponentPropsOptions>, Props = ExtractPropTypes<PropsOptions>> {
    filterProps<T extends Partial<Props>, U extends Exclude<keyof Props, Exclude<keyof Props, keyof T>>>(props: T): [yes: Partial<Pick<T, U>>, no: Omit<T, U>];
}
type DefineComponentWithGenericProps<T extends new (props: Record<string, any>, slots: RawSlots) => {
    $props?: Record<string, any>;
}> = <PropsOptions extends Readonly<ComponentObjectPropsOptions>, 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, I extends ComponentInjectOptions = {}, II extends string = string, Slots extends RawSlots = ConstructorParameters<T>[1], S extends SlotsType = SlotsType<Partial<MakeSlots<Slots>>>, III = InstanceType<T>, P = III extends Record<"$props", any> ? Omit<PropsOptions, keyof III["$props"]> : PropsOptions, Base = DefineComponent<P, RawBindings, D, C, M, Mixin, Extends, E extends any[] ? E : III extends Record<"$props", any> ? Omit<E, ToListeners<keyof III["$props"]>> : E, EE, PublicProps, ExtractPropTypes<P> & ({} extends E ? {} : EmitsToProps<E>), ExtractDefaultPropTypes<P>, S>>(options: ComponentOptionsWithObjectProps<PropsOptions, RawBindings, D, C, M, Mixin, Extends, E, EE, I, II, S>) => Base & T & FilterPropsOptions<PropsOptions>;
type DefineComponentWithSlots<Slots extends RawSlots> = <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, I extends ComponentInjectOptions = {}, II extends string = string, S extends SlotsType = SlotsType<Partial<MakeSlots<Slots>>>>(options: ComponentOptionsWithObjectProps<PropsOptions, RawBindings, D, C, M, Mixin, Extends, E, EE, I, II, S>) => DefineComponent<ExtractPropTypes<PropsOptions> & SlotsToProps<Slots>, RawBindings, D, C, M, Mixin, Extends, E, EE, PublicProps, ExtractPropTypes<PropsOptions> & SlotsToProps<Slots> & ({} extends E ? {} : EmitsToProps<E>), ExtractDefaultPropTypes<PropsOptions>, S> & FilterPropsOptions<PropsOptions>;
export declare function genericComponent(exposeDefaults?: boolean): DefineComponentWithSlots<{
    default: never;
}>;
export declare function genericComponent<T extends new (props: Record<string, any>, slots: any) => {
    $props?: Record<string, any>;
}>(exposeDefaults?: boolean): DefineComponentWithGenericProps<T>;
export declare function genericComponent<Slots extends RawSlots>(exposeDefaults?: boolean): DefineComponentWithSlots<Slots>;
export declare const Comp: DefineComponentWithSlots<{
    default: never;
}>;
export type VBtnSlots = {
    default: never;
    prepend: never;
    append: never;
    loader: never;
};
export declare const VBtn: DefineComponentWithSlots<VBtnSlots>;
export {};

@KaelWD
Copy link
Contributor Author

KaelWD commented Oct 13, 2023

Minimal reproduction:

import type {
  ComponentPropsOptions,
  ComputedOptions,
  MethodOptions,
  ComponentOptionsMixin,
  EmitsOptions,
  SlotsType,
  ComponentOptionsWithObjectProps,
  DefineComponent,
  ComponentInjectOptions,
} from 'vue'

// overload 4 from vue: object format with object props declaration
declare function defineComponent<
  PropsOptions extends Readonly<ComponentPropsOptions>,
  RawBindings,
  D,
  C extends ComputedOptions = {},
  M extends MethodOptions = {},
  Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
  Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
  E extends EmitsOptions = {},
  EE extends string = string,
  S extends SlotsType = {},
  I extends ComponentInjectOptions = {},
  II extends string = string
>(
  options: ComponentOptionsWithObjectProps<
    PropsOptions,
    RawBindings,
    D,
    C,
    M,
    Mixin,
    Extends,
    E,
    EE,
    I,
    II,
    S
  >
): DefineComponent<
  PropsOptions,
  RawBindings,
  D,
  C,
  M,
  Mixin,
  Extends,
  E,
  EE
  /* These are not exported by Vue. DefineComponent has them as defaults anyway so it shouldn't matter
  PublicProps,
  ResolveProps<PropsOptions, E>,
  ExtractDefaultPropTypes<PropsOptions>,
  S */
> // & {}

export const Component = defineComponent({
  props: {
    foo: String,
  },
  setup () {}
})
{
  "include": ["src"],
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "jsx": "preserve",
    "strict": true,
    "declaration": true,
    "outDir": "./dist",
    "moduleResolution": "node"
  }
}

Built file:

import type { ComponentOptionsMixin, DefineComponent } from 'vue';
export declare const Component: DefineComponent<{
    foo: StringConstructor;
}, void, unknown, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {}, string>;

Nothing wrong there, but making the return type an intersection (add & {}) causes typescript to flatten it from DefineComponent<...> to new (...args: any[]): {...}:

import type { ComponentOptionsMixin } from 'vue';
export declare const Component: {
    new (...args: any[]): {
        $: import("vue").ComponentInternalInstance;
        $data: {};
        $props: {
            key?: string | number | symbol | undefined;
            style?: unknown;
            class?: unknown;
            readonly foo?: string | undefined;
            ref?: import("vue").VNodeRef | undefined;
            ref_for?: boolean | undefined;
            ref_key?: string | undefined;
            onVnodeBeforeMount?: ((vnode: import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
                [key: string]: any;
            }>) => void) | ((vnode: import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
                [key: string]: any;
            }>) => void)[] | undefined;
            onVnodeMounted?: ((vnode: import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
                [key: string]: any;
            }>) => void) | ((vnode: import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
                [key: string]: any;
            }>) => void)[] | undefined;
            onVnodeBeforeUpdate?: ((vnode: import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
                [key: string]: any;
            }>, oldVNode: import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
                [key: string]: any;
            }>) => void) | ((vnode: import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
                [key: string]: any;
            }>, oldVNode: import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
                [key: string]: any;
            }>) => void)[] | undefined;
            onVnodeUpdated?: ((vnode: import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
                [key: string]: any;
            }>, oldVNode: import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
                [key: string]: any;
            }>) => void) | ((vnode: import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
                [key: string]: any;
            }>, oldVNode: import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
                [key: string]: any;
            }>) => void)[] | undefined;
            onVnodeBeforeUnmount?: ((vnode: import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
                [key: string]: any;
            }>) => void) | ((vnode: import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
                [key: string]: any;
            }>) => void)[] | undefined;
            onVnodeUnmounted?: ((vnode: import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
                [key: string]: any;
            }>) => void) | ((vnode: import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
                [key: string]: any;
            }>) => void)[] | undefined;
        };
        $attrs: {
            [x: string]: unknown;
        };
        $refs: {
            [x: string]: unknown;
        };
        $slots: Readonly<{
            [name: string]: import("vue").Slot<any> | undefined;
        }>;
        $root: import("vue").ComponentPublicInstance<{}, {}, {}, {}, {}, {}, {}, {}, false, import("vue").ComponentOptionsBase<any, any, any, any, any, any, any, any, any, {}, {}, string, {}>, {}, {}> | null;
        $parent: import("vue").ComponentPublicInstance<{}, {}, {}, {}, {}, {}, {}, {}, false, import("vue").ComponentOptionsBase<any, any, any, any, any, any, any, any, any, {}, {}, string, {}>, {}, {}> | null;
        $emit: (event: string, ...args: any[]) => void;
        $el: any;
        $options: import("vue").ComponentOptionsBase<Readonly<import("vue").ExtractPropTypes<{
            foo: StringConstructor;
        }>>, void, unknown, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {}, string, {}, {}, string, {}> & {
            beforeCreate?: ((() => void) | (() => void)[]) | undefined;
            created?: ((() => void) | (() => void)[]) | undefined;
            beforeMount?: ((() => void) | (() => void)[]) | undefined;
            mounted?: ((() => void) | (() => void)[]) | undefined;
            beforeUpdate?: ((() => void) | (() => void)[]) | undefined;
            updated?: ((() => void) | (() => void)[]) | undefined;
            activated?: ((() => void) | (() => void)[]) | undefined;
            deactivated?: ((() => void) | (() => void)[]) | undefined;
            beforeDestroy?: ((() => void) | (() => void)[]) | undefined;
            beforeUnmount?: ((() => void) | (() => void)[]) | undefined;
            destroyed?: ((() => void) | (() => void)[]) | undefined;
            unmounted?: ((() => void) | (() => void)[]) | undefined;
            renderTracked?: (((e: import("vue").DebuggerEvent) => void) | ((e: import("vue").DebuggerEvent) => void)[]) | undefined;
            renderTriggered?: (((e: import("vue").DebuggerEvent) => void) | ((e: import("vue").DebuggerEvent) => void)[]) | undefined;
            errorCaptured?: (((err: unknown, instance: import("vue").ComponentPublicInstance<{}, {}, {}, {}, {}, {}, {}, {}, false, import("vue").ComponentOptionsBase<any, any, any, any, any, any, any, any, any, {}, {}, string, {}>, {}, {}> | null, info: string) => boolean | void) | ((err: unknown, instance: import("vue").ComponentPublicInstance<{}, {}, {}, {}, {}, {}, {}, {}, false, import("vue").ComponentOptionsBase<any, any, any, any, any, any, any, any, any, {}, {}, string, {}>, {}, {}> | null, info: string) => boolean | void)[]) | undefined;
        };
        $forceUpdate: () => void;
        $nextTick: typeof import("vue").nextTick;
        $watch<T extends string | ((...args: any) => any)>(source: T, cb: T extends (...args: any) => infer R ? (args_0: R, args_1: R) => any : (...args: any) => any, options?: import("vue").WatchOptions<boolean> | undefined): import("vue").WatchStopHandle;
    } & Readonly<import("vue").ExtractPropTypes<{
        foo: StringConstructor;
    }>> & import("vue").ShallowUnwrapRef<{}> & {} & import("vue").ComponentCustomProperties & {};
    __isFragment?: undefined;
    __isTeleport?: undefined;
    __isSuspense?: undefined;
} & import("vue").ComponentOptionsBase<Readonly<import("vue").ExtractPropTypes<{
    foo: StringConstructor;
}>>, void, unknown, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {}, string, {}, {}, string, {}> & import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps;

Now PublicProps is a static part of the library types so can't be modified by consumers. This was fine in Vue 3.2:

import type { ComponentOptionsMixin } from 'vue';
export declare const Component: {
    new (...args: any[]): {
        $: import("vue").ComponentInternalInstance;
        $data: {};
        $props: Partial<{}> & Omit<Readonly<import("vue").ExtractPropTypes<{
            foo: StringConstructor;
        }>> & import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, never>;
        $attrs: {
            [x: string]: unknown;
        };
        $refs: {
            [x: string]: unknown;
        };
        $slots: Readonly<{
            [name: string]: import("vue").Slot | undefined;
        }>;
        $root: import("vue").ComponentPublicInstance<{}, {}, {}, {}, {}, {}, {}, {}, false, import("vue").ComponentOptionsBase<any, any, any, any, any, any, any, any, any, {}, {}, string>, {}> | null;
        $parent: import("vue").ComponentPublicInstance<{}, {}, {}, {}, {}, {}, {}, {}, false, import("vue").ComponentOptionsBase<any, any, any, any, any, any, any, any, any, {}, {}, string>, {}> | null;
        $emit: (event: string, ...args: any[]) => void;
        $el: any;
        $options: import("vue").ComponentOptionsBase<Readonly<import("vue").ExtractPropTypes<{
            foo: StringConstructor;
        }>>, void, unknown, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {}, string, {}, {}, string> & {
            beforeCreate?: ((() => void) | (() => void)[]) | undefined;
            created?: ((() => void) | (() => void)[]) | undefined;
            beforeMount?: ((() => void) | (() => void)[]) | undefined;
            mounted?: ((() => void) | (() => void)[]) | undefined;
            beforeUpdate?: ((() => void) | (() => void)[]) | undefined;
            updated?: ((() => void) | (() => void)[]) | undefined;
            activated?: ((() => void) | (() => void)[]) | undefined;
            deactivated?: ((() => void) | (() => void)[]) | undefined;
            beforeDestroy?: ((() => void) | (() => void)[]) | undefined;
            beforeUnmount?: ((() => void) | (() => void)[]) | undefined;
            destroyed?: ((() => void) | (() => void)[]) | undefined;
            unmounted?: ((() => void) | (() => void)[]) | undefined;
            renderTracked?: (((e: import("vue").DebuggerEvent) => void) | ((e: import("vue").DebuggerEvent) => void)[]) | undefined;
            renderTriggered?: (((e: import("vue").DebuggerEvent) => void) | ((e: import("vue").DebuggerEvent) => void)[]) | undefined;
            errorCaptured?: (((err: unknown, instance: import("vue").ComponentPublicInstance<{}, {}, {}, {}, {}, {}, {}, {}, false, import("vue").ComponentOptionsBase<any, any, any, any, any, any, any, any, any, {}, {}, string>, {}> | null, info: string) => boolean | void) | ((err: unknown, instance: import("vue").ComponentPublicInstance<{}, {}, {}, {}, {}, {}, {}, {}, false, import("vue").ComponentOptionsBase<any, any, any, any, any, any, any, any, any, {}, {}, string>, {}> | null, info: string) => boolean | void)[]) | undefined;
        };
        $forceUpdate: () => void;
        $nextTick: typeof import("vue").nextTick;
        $watch<T extends string | ((...args: any) => any)>(source: T, cb: T extends (...args: any) => infer R ? (args_0: R, args_1: R) => any : (...args: any) => any, options?: import("vue").WatchOptions<boolean> | undefined): import("vue").WatchStopHandle;
    } & Readonly<import("vue").ExtractPropTypes<{
        foo: StringConstructor;
    }>> & import("vue").ShallowUnwrapRef<{}> & {} & import("vue").ComponentCustomProperties & {};
    __isFragment?: undefined;
    __isTeleport?: undefined;
    __isSuspense?: undefined;
} & import("vue").ComponentOptionsBase<Readonly<import("vue").ExtractPropTypes<{
    foo: StringConstructor;
}>>, void, unknown, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, {}, string, {}, {}, string> & import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps;

@KaelWD
Copy link
Contributor Author

KaelWD commented Oct 13, 2023

Looks like we might be able to work around this by defining an intermediary type:

type ExtendDefineComponent<
  Props = {},
  RawBindings = {},
  D = {},
  C extends ComputedOptions = {},
  M extends MethodOptions = {},
  Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
  Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
  E extends EmitsOptions = {},
  EE extends string = string,
  ExtraOptions = {}
> = DefineComponent<
  Props,
  RawBindings,
  D,
  C,
  M,
  Mixin,
  Extends,
  E,
  EE
> & ExtraOptions

@pikax
Copy link
Member

pikax commented Oct 13, 2023

I also got to the same point, with a simple helper

export type MergeComponent<T, M> = T & M;

// change return type
MergeComponent<
  DefineComponent<
    PropsOptions,
    RawBindings,
    D,
    C,
    M,
    Mixin,
    Extends,
    E,
    EE
    /* These are not exported by Vue. DefineComponent has them as defaults anyway so it shouldn't matter
  PublicProps,
  ResolveProps<PropsOptions, E>,
  ExtractDefaultPropTypes<PropsOptions>,
  S */
  >,
  { test: string }
>;

@KaelWD
Copy link
Contributor Author

KaelWD commented Oct 13, 2023

I couldn't get either of these to work in the actual project, it always emits the resolved new (...args: any[]): {...} type even with the most minimal possible component.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
❗ p4-important Priority 4: this fixes bugs that violate documented behavior, or significantly improves perf. regression scope: types
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants
@yyx990803 @sodatea @pikax @ml1nk @KaelWD and others