如何在Vue中解决TS类型错误:MenuOptionWithEx不是MenuOption的子类型?

TS 的 vue 项目中,有如下代码块简化如下:

<n-menu
  :render-extra="renderMenuExtra"
/>

其中,render-extra 报错为:
image.png

项目地址:https://github.com/lingkai5wu/link-admin
问题代码位于这里
类型位于这里

MenuOptionWithEx 是我扩展 MenuOption 的类型,相关信息如下:

type MenuOptionWithEx = MenuVO &
  MenuOption & {
    meta?: RouteMeta
  }

export interface MenuVO {
  id: number
  pid: number
  type: MenuTypeEnum
  label: string | null
  path: string | null
}

// MenuOption 为 Naive UI 内置类型
export type MenuOption = (MenuOptionSharedPart & {
    /** @deprecated */
    title?: string | (() => VNodeChild);
}) | (MenuOptionSharedPart & {
    label?: string | (() => VNodeChild);
});

// 同上
export interface MenuOptionSharedPart {
    key?: Key;
    disabled?: boolean;
    icon?: () => VNodeChild;
    children?: Array<MenuOption | MenuGroupOption | MenuDividerOption>;
    extra?: string | (() => VNodeChild);
    props?: HTMLAttributes;
    show?: boolean;
    [key: string]: unknown;
    /** @deprecated */
    titleExtra?: string | (() => VNodeChild);
}

// RouteMeta 为 Vue Router 内置类型
export declare interface RouteMeta extends Record<string | number | symbol, unknown> {
}

我尝试使用联合类型扩展 MenuOptionWithEx,使 MenuOptionWithEx 为子类型,MenuOption 为父类型,期望满足 TS 的要求,但仍然报错,如上。
经过测试,我发现将 MenuOptionWithEx 中的 MenuVO 从联合类型中移除,就不会出现报错,但这不符合业务逻辑...

我也尝试直接继承 MenuOption,但 MenuOption 是联合类型,似乎没法直接继承。

阅读 613
avatarAI BotBETA

在 TypeScript 中,联合类型(Union Types)表示一个值可以是几种类型之一。在你的例子中,MenuOption 是一个联合类型,这意味着它可以是 MenuOptionSharedPart & { title?: string | (() => VNodeChild); }MenuOptionSharedPart & { label?: string | (() => VNodeChild); } 中的任何一种类型。

由于 MenuOption 是一个联合类型,你不能直接扩展或继承它。但是,你可以通过创建一个新的类型,该类型符合 MenuOption 的所有可能类型的要求,来解决这个问题。

你的 MenuOptionWithEx 类型已经尝试这样做,它结合了 MenuVOMenuOptionRouteMeta。然而,你遇到的问题可能是因为 MenuOptionWithEx 并没有完全符合 MenuOption 的所有可能形状。

一个可能的解决方案是,为 MenuOptionWithEx 创建两个版本,每个版本都对应 MenuOption 的一个可能类型。然后,你可以将这两个版本作为联合类型,以满足 MenuOption 的所有可能要求。例如:

type MenuOptionWithExTitle = MenuVO &
  (MenuOptionSharedPart & {
    title?: string | (() => VNodeChild);
  }) & {
    meta?: RouteMeta;
  };

type MenuOptionWithExLabel = MenuVO &
  (MenuOptionSharedPart & {
    label?: string | (() => VNodeChild);
  }) & {
    meta?: RouteMeta;
  };

type MenuOptionWithEx = MenuOptionWithExTitle | MenuOptionWithExLabel;

这样,MenuOptionWithEx 就成为了 MenuOption 的一个子类型,因为它可以匹配 MenuOption 的所有可能形状。然后,你可以尝试使用 MenuOptionWithEx 作为 renderMenuExtra 的类型,看看是否解决了你的问题。

请注意,这只是一个可能的解决方案,具体的解决方案可能需要根据你的代码库和 MenuOptionSharedPart 的定义进行调整。

2 个回答

这涉及到一个 协变逆变 概念

对于函数参数是反过来,是父类型赋要值给子类型,符合类型安全,可以做个简单的例子:

interface Animal {}
interface Dog extends Animal {
    jiao: () => void
}
interface Cat extends Animal {
    miao: () => void
}

function foo(bar: (arg: Animal) => void) {
    // bar 的参数是 Animal ,猫也是 animal 因此传个猫是可以的
    let cat: Cat = { miao: () => {}}
    bar(cat)
}

// 假设 (a: Dob) => void 能赋值给 (a: Animal) => void
foo((a: Dog) => {
    // 则这里类型不安全,因为这里实际是传入 cat类型,没有 jiao 这个方法
    a.jiao()
})

当然你可以把这个开关关掉 strictFunctionTypes,不过不建议这样子做

强转解决了(忽略变量名和类型名与提问不同)

function renderMenuExtra(menuOption: MenuOption): VNodeChild {
  const menuVOTree = menuOption as unknown as MenuVOTree
  if (menuVOTree.type === 'LINK') {
    return h(NIcon, null, { default: () => h(OpenOutline) })
  }
}
推荐问题
logo
Microsoft
子站问答
访问
宣传栏