我想要创建一个生成器,它可以表示builder.a().b().build()或builder.b().a().build(),但不能代表builder.a().build()、builder.b().build()或builder.a().a().build()等。
显然,我可以在build方法中进行验证,但是我希望编译器能够提示这一点(并且vs代码提供自动完成)。我认为TS类型系统可以使用映射类型、联合和交叉点来表示这个类型,但我无法完全理解它。
有人知道我是怎么做到的吗?
发布于 2019-09-07 07:39:36
让我们从最简单的部分开始,实际的实现。该类使用this作为返回类型。实际的魔术将在以后使用另一种类型发生:
class ConcreteBuilder {
a(): this {
return this;
}
b(): this {
return this;
}
build(): string {
return 'foo';
}
}接下来,我们需要一个泛型类型,在那里我们可以传递一个函数类型,它将返回类型交换为其他类型。你马上就会明白为什么。我从另一个答案复制了这段代码
type ArgumentTypes<T> = T extends (... args: infer U) => infer R ? U: never;
type ReplaceReturnType<O, N> = (...a: ArgumentTypes<O>) => N;现在它变得有趣了,下面是我提出的Builder类型:
type Builder<K extends keyof ConcreteBuilder> = {
[U in K]: U extends 'build' ? ConcreteBuilder[U] : ReplaceReturnType<
ConcreteBuilder[U],
Builder<Exclude<K, U> extends never ? 'build' : Exclude<K, U>>
>
};该类型有一个泛型参数K,它被限制为ConcreteBuilder的一个键。这可能是'a',也可能是'a' | 'b' | 'build'。我们将使用这个参数来确定结果类型中应该可用的方法。
为此,我们使用一个映射类型来迭代K中的所有键。对于每个U in K,除了build方法之外,我们修改该方法的返回类型。
ConcreteBuilder[U]指的是方法的原始函数类型。
如果我们已经到达了构建方法(U extends 'build'),那么将原始类型从ConcreteBuilder中保留下来。否则,我们使用ReplaceReturnType保留原始参数,但将返回类型替换为:
Builder<Exclude<K, U> extends never ? 'build' : Exclude<K, U>>罗得要在这里打开行李。因此,正如您所看到的,我们将返回类型替换为Builder。讨论过的参数定义了哪些方法是可用的。由于这是方法U的返回类型,因此我们希望从可用方法列表中删除U,以防止再次调用它。这是使用类型完成的。在我们的例子中,我们将U从键K的联合中删除。
因为这种递归类型一直持续到从K中删除所有方法,所以我们还需要一个基本情况。这就是条件类型的作用所在。我们检查剩余的键(Exclude<K, U>)是否扩展了never,这实质上意味着是否为“空”。如果是这样的话,返回一个带build方法的生成器。
最后,只剩下builder函数了:
function builder(): Builder<Exclude<keyof ConcreteBuilder, 'build'>> {
return new ConcreteBuilder() as any;
}它返回一个Builder,其中包含除build之外的所有方法。有一个any强制转换是必要的,因为ConcreteBuilder的类型比Builder的限制要小。
全码
type ArgumentTypes<T> = T extends (... args: infer U) => infer R ? U: never;
type ReplaceReturnType<O, N> = (...a: ArgumentTypes<O>) => N;
type Builder<K extends keyof ConcreteBuilder> = {
[U in K]: U extends 'build' ? ConcreteBuilder[U] : ReplaceReturnType<
ConcreteBuilder[U],
Builder<Exclude<K, U> extends never ? 'build' : Exclude<K, U>>
>
};
function builder(): Builder<Exclude<keyof ConcreteBuilder, 'build'>> {
return new ConcreteBuilder() as any;
}
class ConcreteBuilder {
a(): this {
return this;
}
b(): this {
return this;
}
build(): string {
return 'foo';
}
}
builder().a().b().build(); // ok
builder().b().a().build(); // ok
builder().build(); // error
builder().a().build(); // error
builder().b().build(); // error
builder().a().a().build(); // error
builder().b().b().build(); // error
builder().a().b().b().build(); // error
builder().a().a().b().build(); // error游乐场
https://stackoverflow.com/questions/57828972
复制相似问题