从字符串字面量中推断出联合类型

3

假设我想在一个项目中的单个函数中管理来自不同模块的服务,并根据获得的数据类型调用不同的服务。

例如,我有这两个 DTO:

class UserDto {
  constructor (name: string) {
      this.name = name
  }
  public name: string;
}

class CarDto {
  constructor (brand: string) {
      this.brand = brand
  }
  public brand: string;
}

这两项服务:

class UserService {
  create(val: UserDto) {
    // Create the user
    return 'foo';
  }
}

class CarService {
  create(val: CarDto) {
    // Create the car
    return 'bar';
  }
}

相当基础的东西。然后我可以使用一个能够根据接收到的数据类型来管理通话的服务。为此,我创建了一个联合类型,用于确定我接收到的数据类型以及应该调用哪个服务,就像这样:
type DataType =
  | {
      data: 'user';
      dto: UserDto;
      service: 'userService'
    }
  | {
      data: 'car';
      dto: CarDto;
      service: 'carService'
    }


最后,我想把这一切都放在一个管理这两个子服务的服务中的函数中,就像这样:
class MainService {
  constructor (
    private userService: UserService,
    private carService: CarService,
  ) {}

  serviceManager(val: DataType) {
   // Call the right service here!
  }
}

我不想做的是检查每个属性是否是我想要的。这就是我想要避免的事情。
serviceManager(val: DataType) {
   const name = val.data;
    if (name === 'user') this.userService.create(val.dto);
  }

相反,我更喜欢一种方法,即TypeScript可以推断出我获取的服务以及我传递给该服务的信息。所以这就是我认为应该起作用的方式(剧透,它并不起作用)。
serviceManager(val: DataType) {
   const func = this[val.service];
    func.create(val.dto);
    // Argument of type 'UserDto | CarDto' is not assignable to parameter of type 'UserDto & CarDto'.
    // Type 'UserDto' is not assignable to type 'UserDto & CarDto'.
    // Property 'brand' is missing in type 'UserDto' but required in type 'CarDto'.
  }

最后,我决定走另一条路,甚至不使用子服务。但是,即使对于我现在正在处理的项目来说,我一直在努力寻找解决这个问题的方法,尽管它已经不相关了。
使用TypeScript是否可能以某种不同的方式声明DataType类型,或者这根本不可能?也许在声明子服务内部的函数时应该使用泛型?

@jcalz 我相信它是这样的,尽管我对泛型还不太熟悉,所以在尝试调用serviceManager函数时遇到了一点麻烦。如果你能在回答中补充一下这个问题,我会非常感激。否则,我仍然非常感谢! - Emi
1
这样?无论如何,我都会写出一个答案,但请告诉我是否解决了调用serviceManager()方法的问题。 - jcalz
1
这样吗?不管怎样,我会给出答案,但请告诉我是否解决了调用serviceManager()方法的问题。 - jcalz
1
喜欢这个吗?无论如何,我会给出一个答案,但请告诉我是否解决了调用serviceManager()方法的问题。 - undefined
@jcalz,是的,太棒了!非常感谢! - Emi
显示剩余7条评论
1个回答

3

TypeScript不直接支持我所称之为“相关联的联合类型”。在serviceManager()的实现中,val参数是联合类型DataType。因此,this[val.service]val.dto也是联合类型(分别是UserService | CarServiceUserDto | CarDto)。但是TypeScript并没有意识到并且无法追踪这些联合类型在某种程度上是“相关联的”,例如,如果this[val.service]UserService类型,那么val.dto将是UserDto类型。编译器可能认为this[val.service]UserService类型,而val.dtoCarDto类型。然后就会出现错误。这个一般性问题在microsoft/TypeScript#30581中有讨论。

对于这个问题的推荐方法在microsoft/TypeScript#47109中有详细描述。与使用相关联结相反,您的代码应该重构为使用一些基本的键值映射类型,并使用泛型索引来访问这些类型中的元素,以及通过这些类型生成的映射类型的泛型索引。对于您的代码,我倾向于进行如下的重构:
interface DtoMap {
  user: UserDto,
  car: CarDto
}

type ServiceMap =
  { [K in keyof DtoMap]: { create(val: DtoMap[K]): string } }

type DataType<K extends keyof DtoMap> = { [P in K]:
  { data: P, dto: DtoMap[P] }
}[K]

DtoMap 类型是你所需的“基本键值映射类型”,它将你所期望的 DataType 联合类型的 data 属性映射到相应的 DTO 类型。

然后,ServiceMap 描述了与这些 DTO 类型对应的服务对象。

DataType<K> 是一个分布式对象类型,使得 DataType<keyof DtoMap> 成为一个类似于原始 DataType 类型的联合类型(除了我们不需要在这里使用 service;我们可以只写一个类型为 ServiceMapservices 对象)。如果你传递一个单独的键作为 K,你将得到 DataType 联合类型中的那个成员。

现在,我们可以使 serviceManager()K 上具有泛型特性,受限于 DtoMap 的键:

serviceManager<K extends keyof DtoMap>(val: DataType<K>) {
  const services: ServiceMap = {
    car: this.carService,
    user: this.userService
  }
  const func = services[val.data];
  func.create(val.dto);
}

val 输入的类型是 DataType<K>,这意味着 val.data 将确定调用所涉及的联合成员。 services 对象被证明是类��为 ServiceMap 的,因此 const func = services[val.data] 被视为通用类型 { create(val: DtoMap[K]): string }。 这意味着 func.create 的类型是 (val: DtoMap[K]) => string。 由于 val.dto 的类型是 DtoMap[K],这意味着你可以调用 func.create(val.dto)

代码的游乐场链接


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接