我为复杂的数据 struct 实现了构建器模式,其中一些值通过构建器创建:

export interface Builder<Product> {
  build(): Product;
}

export type Element<T> = T extends Array<infer Y> ? Y : T;
export type BuilderInit<Product, Key extends keyof Product> = (
  product: Product,
) => Builder<Element<Product[Key]>>;

abstract class ParentBuilder<Product extends object> {
  readonly #childrenMap = new Map<
    keyof Product,
    BuilderInit<Product, keyof Product>[]
  >();

  protected setChild<Key extends keyof Product>(
    key: Key,
    cb: BuilderInit<Product, Key>,
  ) {
    this.#childrenMap.set(key, [cb]);

    return this;
  }
}

ParentBuilder是一个基类,所有其他构建器都将扩展,它允许为ParentBuilder‘S产品的某些属性添加子构建器.

例如考虑产品:

interface CompanyAssets {
  trucks: Truck[];
}

可能会有Truck人的建造者:

class Truck {
  color!: string;
}

class TruckBuilder implements Builder<Truck> {
  build() {
    return new Truck();
  }
}

接下来,我创建了另一个扩展为ParentBuilder的抽象类.这个类应该用一些额外的方法来增强ParentClass.

abstract class CompanyAssetsBuilder<Product extends CompanyAssets> extends ParentBuilder<Product> {
  setTruck() {
    return this.setChild("trucks", () => {
      return new TruckBuilder() as Builder<Element<Product["trucks"]>>;
    });
  }
}

到目前为止,一切都像预期的那样工作,但当我试图向TrackBuilder添加方法或属性时,问题开始了.然后,TypeScrip认为TruckBuilder不能与我觉得奇怪的Builder<Element<Product["trucks"]>>相比较,因为具体的实现应该可以赋值给回调的返回值.

现在,我按照编译器错误消息的建议将返回值强制转换为unknown,但我想知道为什么会发生这种情况.

Playground

推荐答案

TypeScrip不能将Builder<Element<Product["trucks"]>>TruckBuilder联系起来,因为Product是一个generic类型的参数,而Element是作为conditional type实现的.因此,Element<Product["trucks"]>是所谓的generic conditional type,而TypeScrip无法对这类类型进行太多分析是出了名的.

在使用类型参数指定泛型类型参数之前,它可以是其constraint的任意子类型,而条件类型可以执行几乎任意复杂的操作.编译器不会试图分析这些类型,希望它能对所有可能性做出一些更高阶的声明,而是倾向于只进行defer次求值.因此,在Element<Product["trucks"]>中,编译器不知道Product是什么,而Element是条件类型,所以它基本上放弃了对Element<Product["trucks"]>求值.对于一个人(尤其是编写代码的人)来说,很容易看到Element<Product["trucks"]>并说"Product["trucks"]将是Truck[]的某个子类型,因此Element<Product["trucks"]>将是Truck的某个子类型,因为当给定一个数组类型时,Element返回它的元素类型."但是编译器没有这样的能力.


因此,由于无论如何都是type assertion,所以实际上不需要让编译器相信这些类型是完全可赋值的,只要它们彼此"相关"即可.类型断言的目的是让您做编译器无法验证为安全的事情,但如果所涉及的类型不被视为"相关的",则编译器无论如何都会警告您.这始终可以通过断言与原始表达式和最终类型相关的中间类型来解决.所以如果x as T不起作用,你可以写x as U as T,其中UTtypeof X相关.

那么,TruckBuilderBuilder<Element<Product["trucks"]>>是什么类型的呢?你可以使用the unknown type,作为通用的超类型,它与所有东西相关.但这并不能捕捉到像return TruckBuilder as unknown as Builder<Element<Product["trucks"]>>这样的严重错误.哎呀,我返回的是构造函数,而不是实例.写as unknown as并不一定是wrong,但它比你需要的要强大一些.

您可能希望找到一个"更接近"实际类型的中间类型.例如:Builder<Truck>.编译器发现TruckBuilderBuilder<Truck>相关,因为它是Builder<Truck>的子类型.而且,有趣的是,它也很高兴看到Builder<Truck>Builder<Element<Product["trucks"]>>相关,可能是因为它们都是按照Builder写的.这意味着,它不担心泛型条件类型Element<Product["trucks"]>>,只将两个Builder<⋯>视为相关.

所以这给了我们

return this.setChild("trucks", () => {
  return new TruckBuilder() as
    Builder<Truck> as Builder<Element<Product["trucks"]>>;
});

它编译时没有错误.

Playground link to code

Typescript相关问答推荐

TypScript中的算法运算式

需要使用相同组件重新加载路由变更数据—React TSX

使Typescribe强制所有对象具有相同的 struct ,从两个选项

有没有办法解决这个问题,类型和IntrinsicAttributes类型没有相同的属性?

如何按不能保证的功能过滤按键?

我可以为情态车使用棱角形的路由守卫吗?

TypeScrip 5.3和5.4-对`Readonly<;[...number[],字符串]>;`的测试版处理:有什么变化?

访问继承接口的属性时在html中出错.角形

ANGLE独立组件布线错误:没有ActivatedRouting提供程序

如何使用Geojson模块和@Types/Geojson类型解析TypeScrip中的GeoJSON?

如何使用模板继承从子组件填充基础组件的画布

在正常函数内部调用异步函数

Typescript,如何在通过属性存在过滤后重新分配类型?

接口中可选嵌套属性的类型判断

作为参数的KeyOf变量的类型

从联合提取可调用密钥时,未知类型没有调用签名

如何在TypeScript中将元组与泛型一起使用?

Svelte+EsBuild+Deno-未捕获类型错误:无法读取未定义的属性(读取';$$';)

两个接口的TypeScript并集,其中一个是可选的

Next.JS-13(应用程序路由)中发现无效的中间件