跟踪 TypeScript 中的参数类型
Keep track of argument types in TypeScript
我有一个在 TypeScript 中为我的一些实体实现的构建器模式。这是其中之一(为简单起见已删除)以及 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
.
我已经尝试将泛型类型添加到 class,例如:
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
调用的类型,但实际如何实现它?
这是一个很好的问题,回答起来很愉快!
我们如何让编译器跟踪 class 实例的方法在实例生命周期内收到的所有参数?
哇!这是一个很大的问题!起初我不确定这是否可能。
以下是编译器在 class 实例的生命周期内必须执行的操作:
- 在每次方法调用时,添加实例已收到的参数集。
- 将这些参数分组,以便我们稍后可以对它们进行类型检查。
我们开始...
回答
以下方法很复杂,我只提供了方法签名。我还将那些签名简化为可以表达想法的最低要求。方法实现将相对直接供您提供。
该方法使用累加器类型来跟踪参数类型。这些累加器类型类似于我们将在 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
其形状和动画类型,使用类型联合来扩展适当的泛型类型,然后 return SlideBuilder
。这是答案中最高级的部分。
然后在 start
中,我们使用 never
将 SlideBuilder
初始化为零(可以这么说)。这很有用,因为 T | never
的并集是 T
(类似于 5 + 0 = 5
)。
现在对 withShape
和 withAnimation
的每次调用都使用适当的累加器作为其 return 类型。这意味着每次调用都会适当地扩大类型并将参数分类到适当的桶中!
请注意 withShape
和 withAnimation
泛型 extend string
。这将类型限制为 string
。它还可以防止将字符串文字类型扩大到 string
。这意味着调用者不需要使用 as const
,因此提供了更友好的 API.
结果呢?我们 "keep track of" 参数类型!这里有一些测试表明它如何满足要求。
测试用例
// 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 链接显示了这个答案的演变:
Playground Link 显示仅支持形状且需要 as const
.
的初始解决方案
Playground Link 将动画带入 class 并且仍在使用 as const
。
Playground Link 不再需要 as const
并提供几乎完成的解决方案。
我有一个在 TypeScript 中为我的一些实体实现的构建器模式。这是其中之一(为简单起见已删除)以及 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
.
我已经尝试将泛型类型添加到 class,例如:
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
调用的类型,但实际如何实现它?
这是一个很好的问题,回答起来很愉快!
我们如何让编译器跟踪 class 实例的方法在实例生命周期内收到的所有参数?
哇!这是一个很大的问题!起初我不确定这是否可能。
以下是编译器在 class 实例的生命周期内必须执行的操作:
- 在每次方法调用时,添加实例已收到的参数集。
- 将这些参数分组,以便我们稍后可以对它们进行类型检查。
我们开始...
回答
以下方法很复杂,我只提供了方法签名。我还将那些签名简化为可以表达想法的最低要求。方法实现将相对直接供您提供。
该方法使用累加器类型来跟踪参数类型。这些累加器类型类似于我们将在 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
其形状和动画类型,使用类型联合来扩展适当的泛型类型,然后 return SlideBuilder
。这是答案中最高级的部分。
然后在 start
中,我们使用 never
将 SlideBuilder
初始化为零(可以这么说)。这很有用,因为 T | never
的并集是 T
(类似于 5 + 0 = 5
)。
现在对 withShape
和 withAnimation
的每次调用都使用适当的累加器作为其 return 类型。这意味着每次调用都会适当地扩大类型并将参数分类到适当的桶中!
请注意 withShape
和 withAnimation
泛型 extend string
。这将类型限制为 string
。它还可以防止将字符串文字类型扩大到 string
。这意味着调用者不需要使用 as const
,因此提供了更友好的 API.
结果呢?我们 "keep track of" 参数类型!这里有一些测试表明它如何满足要求。
测试用例
// 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 链接显示了这个答案的演变:
Playground Link 显示仅支持形状且需要 as const
.
Playground Link 将动画带入 class 并且仍在使用 as const
。
Playground Link 不再需要 as const
并提供几乎完成的解决方案。