我在 TypeScript 中为我的一些实体实现了 Builder 模式。这是其中之一(为简单起见已剥离),也是in the playground :
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
的类型调用,但实际如何实现呢?
最佳答案
这是一个很好的问题,回答起来很愉快!
我们如何让编译器跟踪类实例的方法在实例生命周期内收到的所有参数?
哇!这是一个很大的问题!起初我不确定这是否可能。
以下是编译器在类实例的生命周期内必须做的事情:
开始了...
回答
以下方法足够复杂,我只提供了方法签名。我还将这些签名简化为可以表达想法的最低要求。方法实现将为您提供相对简单的方法。
该方法使用累加器类型来跟踪参数类型。这些累加器类型类似于我们将在
Array.reduce
中使用的累加器对象。功能。这里是 the playground link和代码:
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"])
答案的演变
最后,这里有一些游乐场链接,展示了这个答案的演变:
Playground Link显示仅支持形状并需要
as const
的初始解决方案.Playground Link将动画带入类并仍在使用
as const
.Playground Link不再需要
as const
并提供了一个几乎完成的解决方案。
关于typescript - 跟踪 TypeScript 中的参数类型,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/61558542/