跟踪 TypeScript 中的参数类型

Eug*_*kov 4 types builder typescript

我为我的一些实体在 TypeScript 中实现了构建器模式。这是其中之一(为简单起见,将其删除),也在操场上

type Shape = any;
type Slide = any;
type Animation = any;

export class SlideBuilder {

  private slide: Slide;

  public static start() { return new SlideBuilder(); }

  public withShape(name: string, shape: Shape): this {
    this.slide.addShape(name, shape);
    return this;
  }

  public withAnimation(name: string, animation: Animation): this {
    this.slide.addAnimation(name, animation);
    return this;
  }

  public withOrder(shape: string, animations: string[]) {
    this.slide.addOrder(shape, animations);
    return this;
  }
}

SlideBuilder
  .start()
  .withShape("Hello World", {})
  .withAnimation("Animation1", {})
  .withAnimation("Animation2", {})
  .withOrder("Could be 'Hello World' only", ["Could be 'Animation1' or 'Animation2' only"])
Run Code Online (Sandbox Code Playgroud)

问题是,我想添加一种类型检查的可能性,该检查withOrder已使用正确的参数调用,参数已传递给withShapeor withAnimation

我已经尝试向类添加泛型类型,例如:

export class SlideBuilder<S, A> {
  withShape(name: S, shape: Shape)
  withAnimation(name: A, animation: Animation)
  withOrder(shape: S, animation: A[])
}
Run Code Online (Sandbox Code Playgroud)

但我找不到一种方法来跟踪每个调用,例如将调用中的每个类型收集到联合类型中。我知道我需要以某种方式指定调用的类型在withOrder(shape: S1 | S2 | S3 | ... | Sn)哪里,但实际上如何实现它?SnwithShape

Sha*_*tin 5

这是一个很棒的问题,很高兴回答!

我们如何让编译器跟踪类实例的方法在实例的生命周期中收到的所有参数?

哇!这是一个很大的要求!起初我不确定这是否可能。

以下是编译器在类实例的生命周期中必须执行的操作:

  • 在每个方法调用中,添加到实例已收到的参数集中。
  • 将这些参数分组,以便我们稍后可以对它们进行类型检查。

开始了...

回答

以下方法非常复杂,我只提供了方法签名。我还将这些签名简化为可以表达该想法的最低要求。您提供的方法实现将相对简单。

该方法使用累加器类型来跟踪参数类型。这些累加器类型类似于我们在Array.reduce函数中使用的累加器对象。

这是游乐场链接和代码:

type TrackShapes<TSlideBuilder, TNextShape> = 
  TSlideBuilder extends SlideBuilder<infer TShapes, infer TAnimations> 
  ? SlideBuilder<TShapes | TNextShape, TAnimations> 
  : never;

type TrackAnimations<TSlideBuilder, TNextAnimation> = 
  TSlideBuilder extends SlideBuilder<infer TShapes, infer TAnimations> 
  ? SlideBuilder<TShapes, TAnimations | TNextAnimation> 
  : never;

export class SlideBuilder<TShape, TAnimation> {

  public static start(): SlideBuilder<never, never> {
    return new SlideBuilder<never, never>();
  };

  public withShape<TNext extends string>(name: TNext): TrackShapes<this, TNext> {
      throw new Error('TODO Implement withShape.');
  }

  public withAnimation<TNext extends string>(name: TNext): TrackAnimations<this, TNext> {
      throw new Error('TODO Implement withAnimation.');
  }

  public withOrder(shape: TShape, animation: TAnimation[]): this {
    throw new Error('TODO Implement withOrder.');
  }
}
Run Code Online (Sandbox Code Playgroud)

那里发生了什么事?

我们为 定义了两种累加器类型SlideBuilder。它们接收现有的SlideBuilderinfer其形状和动画类型,使用类型联合来扩展适当的通用类型,然后返回SlideBuilder. 这是答案中最高级的部分。

然后在 内部start,我们使用never来初始化SlideBuilder为零(可以这么说)。T | never这很有用,因为is的并集T(类似于 how 5 + 0 = 5)。

现在,每次调用withShapewithAnimation使用适当的累加器作为其返回类型。这意味着每次调用都会适当地扩展类型并将参数分类到适当的存储桶中!

请注意withShapewithAnimation泛型extend string。这将类型限制为string. 它还可以防止将字符串文字类型扩展为string. 这意味着调用者不需要使用as const,从而提供了更友好的 API。

结果?我们“跟踪”参数类型!以下是一些测试,显示它如何满足要求。

测试用例

// Passes type checking.
SlideBuilder
  .start()
  .withShape("Shape1")
  .withAnimation('Animation1')
  .withOrder("Shape1", ["Animation1"])

// Passes type checking.
SlideBuilder
  .start()
  .withShape("Shape1")
  .withAnimation('Animation1')
  .withAnimation('Animation2')
  .withOrder("Shape1", ["Animation1", "Animation2"])

// Fails type checking.
SlideBuilder
  .start()
  .withShape("Shape1")
  .withAnimation('Animation1')
  .withAnimation('Animation2')
  .withOrder("Foo", ["Animation1", "Animation2"])

// Fails type checking.
SlideBuilder
  .start()
  .withShape("Shape1")
  .withAnimation('Animation1')
  .withAnimation('Animation2')
  .withOrder("Shape1", ["Foo", "Animation2"])
Run Code Online (Sandbox Code Playgroud)

答案的演变

最后,这里有一些游乐场链接,显示了这个答案的演变:

Playground Link显示仅支持形状且需要 的初始解决方案as const

Playground Link将动画带入类中并仍在使用as const.

Playground Link消除了对 Playground Link 的需求as const,并提供了一个几乎完成的解决方案。