首页 > 解决方案 > 使用打字稿扩展/继承泛型时遇到问题

问题描述

我在打字稿中将泛型与接口扩展相结合时遇到了麻烦。我的基本用例是这样的:

  1. 基础接口
  2. 从父接口扩展的子接口(只有 1 级深度继承)
  3. 每个子接口都包含不在基本接口中的数据
  4. 某些字段可能会或可能不会在各种兄弟接口之间共享

我希望能够编写一个类型安全的泛型函数,该函数可以正确识别子接口,包括适当地匹配泛型接口的参数。

我已经成功地为扩展接口使用了鉴别器,但是在将它与其他参数联系起来时遇到了麻烦。例如:

这是我们的基本类型,以及一个鉴别器值“Type”:

interface Base<T> {
    type: T
    a: string
    b: number
}

以下是可能的扩展/继承者:

type ExtensionType = 'CDate' | 'DBool' | 'CString'

interface ExtensionCDate<T = 'CDate'> extends Base<T> {
    c: Date
}

interface ExtensionDBool<T = 'DBool'> extends Base<T> {
    d: boolean
}

interface ExtensionCString<T = 'CString'> extends Base<T> {
    c: string
}

这里的想法是 ExtensionCDate 只能采用 'CDate' 的通用值,因此它的类型值始终是 'CDate' 等。

这是我试图解决这个问题的方法:

  1. 使用可能的子接口的联合类型:
type GenericExtension<T extends ExtensionType> = ExtensionCDate | ExtensionDBool | ExtensionCString
  1. 为每个子接口生成一个对应的接口,该接口仅包含子接口中存在的字段,而基接口中不存在(本质上是设置差异):
type ExtendedData<T extends ExtensionType> = Omit<GenericExtension<T>, keyof Base<T>>
  1. 编写一个函数,使用歧视来正确处理每个可能的孩子:
const genericFunc = <T extends ExtensionType>(obj: GenericExtension<T>, data: ExtendedData<T>): void => {
    switch ( obj.type ) {
        case 'CDate':
            obj.c = data.c  // data.c should exist
            return
        case 'DBool':
            obj.d = data.d  // data.d should exist
            return 
        case 'CString':
            obj.c = data.c  // data.c should exist
            return
    }
}

不幸的是,虽然 obj 的输入似乎有效,但鉴别器也不适用于 data 参数。我知道我可以使用强制转换 (data.c as ExcludedData<'CDate'>),但我发现它有一个 hacky 解决方案,并不理想。

我得到的错误如下:

TS2339: Property 'c' does not exist on type 'Pick  , never>'.

这让我觉得我至少错误地使用了 ExtendedData 类型。我觉得解决方案应该相当简单。我在这里想念什么?

谢谢!让我知道我是否可以提供任何额外的上下文来帮助提出解决方案。

标签: inheritancetypescript-genericstype-safety

解决方案


泛型(取决于变量 subtype T)和特定子类型的实例之间存在一些混淆。

type GenericExtension<T extends ExtensionType> = ExtensionCDate | ExtensionDBool | ExtensionCString

在上面的行中,您已经声明了一个泛型T,但实际上并没有T在您的类型定义中使用。

interface ExtensionCDate<T = 'CDate'> extends Base<T> {
    c: Date
}

在特定的扩展中,您实际上所做的<T = 'CDate'>是将字符串文字设置为if未设置CDate默认值,但实际上它仍然可以设置为任何值,因为我们没有对其施加任何限制。TT

您想要的是您的特定扩展类型不再采用通用T变量。相反,我们手动T设置Base<T>.

interface ExtensionCDate extends Base<'CDate'> {
    c: Date
}

interface ExtensionDBool extends Base<'DBool'> {
    d: boolean
}

interface ExtensionCString extends Base<'CString'> {
    c: string
}

由于我们GenericExtension实际上并不是打字稿意义上的“通用”,所以让我们AnyExtension改为调用它。这种类型是三种特定扩展类型的联合。

type AnyExtension = ExtensionCDate | ExtensionDBool | ExtensionCString

我们实际上可以从中导出三种类型字符串的并集,而不是写出来。

type ExtensionType = AnyExtension['type'] // evaluates to type "CDate" | "DBool" | "CString"

我们仍然会遇到该switch语句的一些问题,因为基于切换obj.type改进了打字稿对 T 类型obj不是 T类型的了解。即使您知道如果obj.type === "CDate"必须T如此, typescript"CDate"也不会实现这一飞跃,因此不会data基于switch.

不幸的是,我认为您确实需要某种as断言,至少在data变量上是这样。


推荐阅读