【问题标题】:TypeScript Generic uses default type instead of given typeTypeScript Generic 使用默认类型而不是给定类型
【发布时间】:2021-12-02 06:53:52
【问题描述】:

我正在编写insert post 用例的实现。我正在研究 Clean Architecture,我想创建一个博客 API,您可以在其中创建、阅读、更新和删除帖子。

清洁架构原则说,您必须将应用程序隔离到多个层中,每个层都负责尽可能地抽象您的代码。我将我的代码分为三个主要层:core(最“不可变”的抽象,我在其中定义数据模型和用例),app(我在其中编写主要应用程序代码,如验证和其他内容)和resources(存放不太抽象的代码,如数据库连接、适配器等)。

一般数据被抽象到core 层,因为它是我的API 中最基本的信息。但是一般数据(特别是 idcreated_atupdated_atis_deleted 字段)被抽象到 app 层,因为它依赖于数据库 - id 字段可以是一个数字(对于一般SQL 数据库)或字符串(例如MongoDB)。然后,我只是将这两个抽象结合到一个模型中来创建“确定的”应用程序模型。

但是这样做有一个问题:用例抽象使用id 数据来定义其实现规则。例如,您有InsertPost 用例,其中返回数据是新创建帖子的id。由于id 只定义在app 层,而Clean Architecture 不允许你从更多外部层请求数据,这个抽象怎么知道他返回的是什么类型的id?我找到的解决方案是将id类型抽象为两种可能的类型(numberstring),并且只在app层提供确定的类型。

这是所描述的InsertPost 用例:

// ----- IMPORTS -----
type Id = number | string;

interface Post<IdType extends Id = Id> {
    title: string
    description: string
    body: string
    author_id: IdType
}

interface SuccessfulResponse<T> {
    results: Array<T>
}
// -------------------

export namespace InsertPost { 
    export type Params<IdParam extends Id = Id> = Post<IdParam> 
} 
 
export interface InsertPost<IdType extends Id> { 
    insert: (params: InsertPost.Params) => Promise<SuccessfulResponse<IdType>>
}

我从InsertPost 命名空间中编写了Params 类型,使其具有只接受从Id 类型派生的类型的通用类型。当没有给出类型时,IdType 也应该有一个默认类型。我的问题是 TypeScript 使用默认的 Id 类型,无论是否提供了有效的替代类型。

这是我对InsertPost 用例的实现:

// ----- IMPORTS -----
type Id = number | string;

interface Post<IdType extends Id = Id> {
    title: string
    description: string
    body: string
    author_id: IdType
}

interface SuccessfulResponse<T> {
    results: Array<T>
}

namespace DefaultData {
    export type Id = number
    export type IsDeleted = boolean
    export type CreatedAt = Date
    export type UpdatedAt = Date
}

interface DefaultData {
    id: DefaultData.Id
    is_deleted: DefaultData.IsDeleted
    created_at: DefaultData.CreatedAt
    updated_at: DefaultData.UpdatedAt
}

interface InsertPostRepository  {
    insertPost: (user: Post) => Promise<DefaultData.Id>
}

function successfulResponseDataFormatter<T>(data: Array<T> | T): SuccessfulResponse<T> {
    return {
        results: Array.isArray(data) ? data : [data]
    };
}

namespace InsertPost {
    export type Params<IdParam extends Id = Id> = Post<IdParam>
}

interface InsertPost<IdType extends Id> {
    insert: (params: InsertPost.Params) => Promise<SuccessfulResponse<IdType>>
}
// -------------------

export class InsertPostService implements InsertPost<DefaultData.Id> {
    constructor(private readonly insertPostRepository: InsertPostRepository) {}

    async insert(params: InsertPost.Params<DefaultData.Id>): Promise<SuccessfulResponse<DefaultData.Id>> { // TypeScript throws an error at this line
        const { title, description, body, author_id } = params;

        // Validation functions here

        const id = await this.insertPostRepository.insertPost({
            title,
            description,
            body,
            author_id
        });

        return successfulResponseDataFormatter(id);
    }
}

(Here's a link to this code)

TypeScript 抛出以下错误:

“InsertPostService”类型中的属性“插入”不能分配给基类型“InsertPost”中的相同属性。

类型 '(params: Params) => Promise' 不可分配给类型 '(params: Params) => Promise'。

参数“params”和“params”的类型不兼容。

类型“Params”不可分配给类型“Params”。

类型 'Id' 不能分配给类型 'number'。

类型“字符串”不可分配给类型“数字”。

根据我的测试,这只发生在我从namespace 中导出类型时(不幸的是,我需要使用它)。我真的看不出哪里出错了;有什么我缺少的代码吗,或者这是一个 TypeScript 错误?

【问题讨论】:

  • 请提供minimal reproducible example,清楚地表明您面临的问题。理想情况下,有人可以将代码放入像The TypeScript Playground (link here!) 这样的独立IDE 中,然后立即着手解决问题,而无需首先重新创建它。所以应该没有错别字、不相关的错误或未声明的类型或值。
  • @jcalz 完成!请说现在是否足够好。
  • this 是否满足您的需求?如果是这样,我可以写一个答案;如果没有,请考虑 editing 示例代码来演示失败的用例。
  • @jcalz 没有想到这个解决方案,我感觉很愚蠢!是的,这解决了我的问题。非常感谢!

标签: typescript typescript-generics


【解决方案1】:

如您的示例中所写,InsertPost&lt;XXX&gt; 需要有一个insert 方法,其参数类型为InsertPost.Params,其计算结果为Post&lt;Id&gt;;请注意这是如何独立于XXX 的。但是您的InsertPostService 有一个insert 方法,其参数类型为InsertPost.Params&lt;DefaultData.Id&gt;,其计算结果为Post&lt;DefaultData.Id&gt;。这与Post&lt;Id&gt; 的类型不同,因此会出现编译器错误。 parameter bivariance 可能存在一些技术问题,但这是您遇到的基本问题。

大概你希望InsertPost&lt;XXX&gt; 有一个insert 方法,其参数取决于XXX。鉴于您的其他代码,我能想到的最合理的候选者应该是InsertPost.Params&lt;XXX&gt; 类型:

interface InsertPost<IdType extends Id> {
    insert: (params: InsertPost.Params<IdType>) => Promise<SuccessfulResponse<IdType>>
    // -----------------------------> ^^^^^^^^ <--- added this
}

一旦我这样做了,您的代码就会开始编译,因为您的 InsertPostService 实现了 InsertPost&lt;DefaultData.Id&gt;。您总是有可能遇到其他问题,但这至少是合理的第一步。

Playground link to code

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-09-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-10-15
    • 2021-12-22
    • 2021-01-29
    相关资源
    最近更新 更多