如何使用多态性通过“transformer”类继承将对象从一种类型转换为另一种类型

How to use polymorphism to transform an object from one type into another with "transformer" classes inheritance

提问人:Jonas Pauthier 提问时间:11/11/2023 最后编辑:Jonas Pauthier 更新时间:11/13/2023 访问量:24

问:

我正在开发一个 GraphQL API。GraphQL 类型和数据库类型不一样,因为我不希望我的数据库特定结构泄露给 API 使用者。基本上,我想重命名属性或从手头的对象中删除属性,具体取决于我是将数据返回给使用者还是将对象发送到数据库。GraphQL 中的类型具有属性,但在数据库端,它们具有 .id: string_id: string, _key: string

这组转换对于所有顶级类型都是全局的,因此我想到使用一个基本的“Transformer”类来实现该共享逻辑。然后,API 中的每个域都可以有一个特定的类来扩展基类,但根据它所处理的特定类型提供更多转换。我命名了来自 API 层的类型和来自数据库的类型。ModelEntity

这是我最终得到的实现,但我不确定这是键入它的最佳方式:

// base-transformer.ts

export interface Entity {
  _id: string;
  _key: string;
}

export interface Model {
  id: string;
}

export class BaseTransformer {
  toEntity(model: Model): Entity {
    const { id, ...rest } = model;
    const [, key] = id.split("/");

    return { ...rest, _id: id, _key: key };
  }

  toModel(entity: Entity): Model {
    const { _id, _key, ...rest } = entity;
    return { ...rest, id: _id };
  }
}
// car-transformer.ts

import { BaseTransformer, type Model, type Entity } from "./base-transformer";

export interface CarModel extends Model {
  plateNumber: string;
}

type CarEntity = Omit<CarModel, "id"> & Entity;

export class CarTransformer extends BaseTransformer {
  toEntity(model: CarModel): CarEntity {
    // that's the part that troubles me. I tried not to have to cast the return
    // but obviously with polymorphism you can get to the most child type to the 
    // parent type but not the other way around.
    return super.toEntity(model) as CarEntity;
  }

  toModel(entity: CarEntity): CarModel {
    return super.toModel(entity) as CarModel;
  }
}

我还尝试了绑定泛型类型,但后来我最终得到了泛型抽象的子类型可能与泛型扩展的类型不同。我找到的解决方案是将回调传递给其工作是将父类型转换为泛型类型的方法。我觉得这首先会破坏这个架构的目的,因为我必须做与该类相同的工作,并将其用于每个子类。这是它的样子:TS Error 2322BaseTransformerTBaseTransformer

// base-transformer.ts

export interface Entity {
  _id: string;
  _key: string;
}

export interface Model {
  id: string;
}

export class BaseTransformer {
  toEntity<R extends Entity>(model: Model): R {
    const { id, ...rest } = model;
    const [, key] = id.split("/");

    return { ...rest, _id: id, _key: key }; // TS Error 2322 here
  }

  toModel<R extends Model>(entity: Entity): R {
    const { _id, _key, ...rest } = entity;
    return { ...rest, id: _id }; // TS Error 2322 here
  }
}
TypeScript 多态性 TypeScript-generics TypeScript-Types

评论

0赞 jcalz 11/11/2023
这种方法是否满足您的需求?如果是这样,我会写一个答案来解释;如果没有,我错过了什么?
0赞 Jonas Pauthier 11/11/2023
谢谢@jcalz。这绝对看起来像我想要实现的目标。不过,我一直在摆弄你的例子,我注意到你没有在类上给出返回类型。如果我指出,那么它不起作用。我们是否需要像您那样制作实用程序类型?toModelBaseTransformer: ModeltoModeltoEntity
0赞 jcalz 11/11/2023
你能证明你为什么需要它吗?您可以手动返回推断的返回类型,但为什么呢?如果您希望返回,那么不幸的是,编译器无法抽象以查看 和 的泛型等价性。但是对于特定的实例,它可以如此具体的子类应该没问题。如果你真的需要,我只需在返回行中写(或)。但在我考虑这个问题之前,我希望看到一个最小的可重复的例子,其中这样的事情是必要的。我应该如何进行?toModel()MOmitOmit<ToEntity<M>, "_id" | "_key"> & { id: string; }MMas Mas Model as MtoModel()
0赞 Jonas Pauthier 11/13/2023
老实说,我没有特别的需要。我试图找到键入此场景的惯用方式,我认为最好同时键入方法的参数和返回类型,但也许我错了。我的目标是提高我的 TypeScript 技能,所以我想问问其他人他们有什么建议,如果有的话。在这件事上,你似乎比我更了解,所以如果你认为这是正确的输入方式,我准备接受你的解决方案作为答案。

答:

1赞 jcalz 11/13/2023 #1

唯一可行的方法是使基类在特定于每个子类的模型类型中泛型M

class BaseTransformer<M extends Model> {
  toEntity(model: M): ToEntity<M> {
    const { id, ...rest } = model;
    const [, key] = id.split("/");

    return { ...rest, _id: id, _key: key };
  }

  toModel(entity: ToEntity<M>) {
    const { _id, _key, ...rest } = entity;
    return { ...rest, id: _id };
  }
}

返回类型等效于(使用 Omit 实用程序类型来抑制一组键,并使用 intersection 来添加一组键),所以为了节省空间,我定义了一个实用程序类型:toEntity()Omit<M, "id"> & EntityToEntity<M>

type ToEntity<M extends Model> = Omit<M, "id"> & Entity;

这意味着 的输入类型应为 。我没有注释它的返回类型,编译器推断为 .从概念上讲,它应该只是 ,但不幸的是,如果您尝试将其注释为这样,您将收到一个错误:toModelToEntity<M>Omit<ToEntity<M>, "_id" | "_key"> & { id: string; }M

toModel(entity: ToEntity<M>): M {
  const { _id, _key, ...rest } = entity;
  return { ...rest, id: _id }; // error!
  // ^-- Type 'Omit<ToEntity<M>, "_id" | "_key"> & { id: string; }' 
  // is not assignable to type 'M'.
}

这是因为类型检查器无法对泛型类型的抽象不变属性执行任意高阶推理,尤其是当它们涉及 中使用的条件类型时(这取决于 Exclude,这是一个条件类型)。是的,可能与实际情况相同(从技术上讲,它们可能不同;例如,属性可能是 like 的子类型),但类型检查器看不到它。OmitOmit<Omit<M, "id"> & Entity, "_id" | "_key"> & { id: string }Midstring"foo"

因此,有两种方法可以进行。要么你只是断言返回类型是 ,M

toModel(entity: ToEntity<M>) {
  const { _id, _key, ...rest } = entity;
  return { ...rest, id: _id } as Model as M;
}

或者,您可以不带注释返回类型。我选择不带注释的返回类型。在正确实现子类的情况下,这无关紧要:

interface CarModel extends Model {
  plateNumber: string;
}
type CarEntity = Omit<CarModel, "id"> & Entity;

export class CarTransformer extends BaseTransformer<CarModel> {
  toEntity(model: CarModel): CarEntity {
    return super.toEntity(model);
  }

  toModel(entity: CarEntity): CarModel {
    return super.toModel(entity);
  }
}

但是,如果 和 的返回类型的等价性被破坏,您将收到一个错误,希望会有所帮助:MtoModel()

interface OopsieModel extends Model {
  id: "foo" // <-- narrower than string
  x: number
}
interface OopsieEntity extends Entity {
  x: number
}
class OopsieTransformer extends BaseTransformer<OopsieModel> {
  toEntity(model: OopsieModel): OopsieEntity {
    return super.toEntity(model);
  }
  toModel(entity: OopsieEntity): OopsieModel {
    return super.toModel(entity) // error! 
    // Type 'string' is not assignable to type '"foo"'
  }
}

但这实际上取决于你想采用哪种方法的用例。

Playground 代码链接