【问题标题】:Trouble extending/inheriting generics with typescript使用打字稿扩展/继承泛型时遇到问题
【发布时间】:2021-01-15 07:41:56
【问题描述】:

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

  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),但我发现这有一个 hacky 解决方案,并不理想。

我得到的错误如下:

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

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

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

【问题讨论】:

    标签: inheritance typescript-generics type-safety


    【解决方案1】:

    在依赖于变量子类型T 的泛型和特定子类型的实例之间存在一些混淆。

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

    在上一行中,您声明了一个泛型 T,但实际上并未在类型定义中使用 T。

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

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

    您希望您的特定扩展类型不再采用通用的T 变量。相反,我们手动将T 设置为Base&lt;T&gt;。

    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 的切换改进了打字稿对obj 类型的了解,但不是T。即使您知道如果 obj.type === "CDate" 和 T 也必须是 "CDate",typescript 不会实现这一飞跃,因此不会改进其对 data 类型的了解基于switch。

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

    【讨论】:

    • 感谢您的回复!从那以后,我放弃了让它发挥作用,并最终采取了不同的路线。不幸的是打字稿不允许它干净地工作!
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-07-26
    • 2022-01-11
    • 2020-07-19
    • 1970-01-01
    • 1970-01-01
    • 2011-08-20
    相关资源
    最近更新 更多