我在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"])
问题是,我想增加一种可能性,以便进行类型检查。withOrder
调用了正确的参数,这些参数已经传递给了 withShape
或 withAnimation
.
我已经尝试过在类中添加通用类型,比如。
export class SlideBuilder<S, A> {
withShape(name: S, shape: Shape)
withAnimation(name: A, animation: Animation)
withOrder(shape: S, animation: A[])
}
但我找不到一种方法来跟踪每一次调用, 比如把每一个类型从调用中收集到联合类型. 我知道我需要以某种方式指定 withOrder(shape: S1 | S2 | S3 | ... | Sn)
哪儿 Sn
是一种来自 withShape
调用,但如何真正实现它?
这是一个很精彩的问题,回答起来很愉快!我们如何让编译器跟踪我们的实体?
我们如何让编译器跟踪一个类实例的方法在其生命周期内收到的所有参数?
Whew! 这是个大问题! 一开始我还不确定这是否可能。
下面是编译器在类实例的生命周期中必须做的事情。
在这里,我们开始...
下面的方法非常复杂,我只提供了方法的签名。我也将这些签名简化为能够表达思想的最低要求。方法的实现对你来说将是比较简单的。
该方法使用累加器类型来跟踪参数类型。这些累加器类型类似于我们在一个 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.');
}
}
我们定义了两个累加器类型 SlideBuilder
. 这些都收到了现有的 SlideBuilder
, infer
它的形状和动画类型,使用类型联盟来拓宽适当的通用类型,然后返回 SlideBuilder
. 这是答案中最高级的部分。
然后在里面 start
,我们使用 never
来初始化 SlideBuilder
在零点(可以说)。这很有用,因为 T | never
是 T
(类似于如何 5 + 0 = 5
).
现在每次调用 withShape
和 withAnimation
使用适当的累加器作为其返回类型。这意味着每次调用都会适当地拓宽类型,并将参数归类到相应的桶中!
注意 withShape
和 withAnimation
仿制药 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"])
最后,这里有一些操场链接,可以展示这个答案的演变过程。
游戏场链接 显示了只支持形状的初始解决方案,并且只需要 as const
.
遊樂場連結 将动画带入类中,并且仍在使用 as const
.
遊樂場連結 免去了 as const
并提供了一个几乎完成的解决方案。