【问题标题】:TypeScript cyclic/recursive interface definitionTypeScript 循环/递归接口定义
【发布时间】:2020-10-17 19:55:41
【问题描述】:

基本情况

我在定义一个以模块化方式满足我需求的模块化类型系统时遇到了麻烦。这是我的问题的简单版本:

我需要几乎相同类型系统的两种略有不同的表示:一种用于前端,一种用于后端(稍后我将解释差异,现在这仅用于上下文)。我正在尝试定义一个类型系统,我可以在其中从另一个派生一个,因为这在理论上是可行的。

以下是理解模型的方法:

  • 一个User 有多个Teams。
  • 一个Team 有多个Units。

这就是概念上的全部内容:(A)

type IUser = {
    username: string,
    teams: ITeam[],
};

type ITeam = {
    teamname: string,
    user: IUser ,
    units: IUnit[],
}

type IUnit = {
    unitname: string,
    team: ITeam ,
}

我需要能够以两种方式访问​​每个关系(例如User -> TeamTeam -> User)。从用户到单位,从单位到用户。

上下文

这很适合输入我的前端将使用的内容。现在,这是我需要在后端表示的内容:我使用的是 TypeORM,所以它们中的每一个都是实体,而实际代码看起来像这样:

(B)(这也主要用于上下文,有一个游乐场剥离了任何 TypeORM 逻辑较低)

import {Entity, Column, OneToMany, ManyToOne} from "typeorm";

@Entity()
class User extends BaseEntity {
    @Column()
    username: string,

    // A User has many Teams, loaded lazily
    @OneToMany(() => Team, (team) => team.user, {})
    teams: Promise<Team[]>,
};

@Entity()
class Team extends BaseEntity {
    @Column()
    teamname: string,

    // A Team is owned by one User, loaded eagerly
    @ManyToOne(() => User, (user) => user.teams, {
        eager: true,
    })
    user: User,

    // A Team has many Units, loaded lazily
    @OneToMany(() => Unit, (unit) => unit.team, {})
    units: Promise<Unit[]>,
}

@Entity()
class Unit extends BaseEntity {
    @Column()
    unitname: string,

    // A Unit is owned by one Team, loaded eagerly
    @ManyToOne(() => Team, (team) => team.units, {
        eager: true,
    })
    team: Team,
}

当然,正如the documentation of TypeORM 告诉我们的那样:

Eager 关系只能用于关系的一侧,不允许在关系的两侧使用 eager: true。

所以我选择了一个所有关系都会急切加载的方向,而另一个他们会延迟加载的方向。我决定懒惰地加载下降方式(因为在我的用例中,我不需要每次都需要用户的每个团队),并急切地加载上升方式(因为我想知道每个单位的用户我的用例中的时间)。

我想要什么

我想验证模型 (B) 是否只是具有一些承诺字段的模型 (A)。


问题

现在我无法验证模型 (B) 是否与模型 (A) 有任何关系,因为字段引用的某些属性也已被承诺。


我尝试了什么

您可以按照这个 TypeScript 游乐场链接进行操作,该链接消除了 TypeORM 上下文的这些问题,并专注于实际的类型定义:Playground link

我已经列出了我之前描述的两个模型,以及我目前关于如何使用一个模型从另一个模型中推导出来的推理。但是现在我陷入了困境:您可以阅读到操场的尽头,以了解我需要某种我不知道如何表达的递归。

【问题讨论】:

    标签: typescript types typeorm


    【解决方案1】:

    我解决这个问题的方法是确保前端和后端版本都实现了一些共享接口。对于这个共享接口,我说一个值可以是一个承诺或通过创建实用程序类型MaybePromise&lt;T&gt; 解析的值:

    type MaybePromise<T> = T | Promise<T>;
    

    使用此实用程序类型,我将I 接口重新定义为共享基,它可能有也可能没有承诺。

    type IUser = {
        username: string,
        teams: MaybePromise<ITeam[]>,
    };
    
    type ITeam = {
        teamname: string,
        user: MaybePromise<IUser>,
        units: MaybePromise<IUnit[]>,
    }
    
    type IUnit = {
        unitname: string,
        team: MaybePromise<ITeam>,
    }
    

    如果我们想取回原始的非承诺接口,我们可以使用另一种实用程序类型来实现,它将这些可能的承诺替换为它们的解析值。

    type UnpromisifyFields<T> = {
        [key in keyof T]: T[key] extends MaybePromise<infer U> ? UnpromisifyFields<U> : T[key]
    }
    

    进行这种递归意味着解析的类型不是特别可读,但它们似乎是正确的。

    type PureUser = UnpromisifyFields<IUser>
    // becomes { username: string; teams: UnpromisifyFields<ITeam>[]; }
    
    type PureTeam = UnpromisifyFields<ITeam>
    
    type PureUnit = UnpromisifyFields<IUnit>
    

    我们的后端类能够实现基本的MaybePromise 接口:

    class User implements IUser {    
        username!: string;
        teams!: Promise<Team[]>;
    };
    
    class Team implements ITeam {
        teamname!: string;
        user!: User;
        units!: Promise<Unit[]>;
    }
    
    class Unit implements IUnit {
        unitname!: string;
        team!: Team;
    }
    

    我认为这可以解决您的大部分问题。

    【讨论】:

    • 哦,对了。实际上,我确实在某个时候遇到过这个问题,并且我认为我可以做得更好,但我认为那时它已经过度设计了。谢谢
    猜你喜欢
    • 1970-01-01
    • 2021-12-27
    • 2015-07-27
    • 1970-01-01
    • 2018-06-25
    • 2018-06-27
    • 2017-01-21
    • 2020-11-24
    • 1970-01-01
    相关资源
    最近更新 更多