我想创建一个可以代表builder.a().b().build()or 的构建器builder.b().a().build(),但不能代表builder.a().build(), builder.b().build()orbuilder.a().a().build()等。
显然我可以在构建方法中进行验证,但我希望编译器对此进行提示(并让 vs code 提供自动完成功能)。我认为 TS 类型系统可以使用映射类型、并集和交集来表示这一点,但我不太明白。
有谁知道我该怎么做?
让我们从最简单的部分开始,即实际的实现。该类用作this返回类型。稍后将使用附加类型来实现实际的魔法:
class ConcreteBuilder {
a(): this {
return this;
}
b(): this {
return this;
}
build(): string {
return 'foo';
}
}
Run Code Online (Sandbox Code Playgroud)
接下来,我们需要一个泛型类型,我们可以在其中传递函数类型,并将返回类型交换为其他类型。您很快就会明白原因。我从另一个答案复制了这段代码:
type ArgumentTypes<T> = T extends (... args: infer U) => infer R ? U: never;
type ReplaceReturnType<O, N> = (...a: ArgumentTypes<O>) => N;
Run Code Online (Sandbox Code Playgroud)
现在它变得有趣了,这是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>>
>
};
Run Code Online (Sandbox Code Playgroud)
该类型有一个通用参数,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>>
Run Code Online (Sandbox Code Playgroud)
这里有很多东西需要解压。正如您所看到的,我们将返回类型替换为Builder. 正如所讨论的,参数定义了可用的方法。由于这是 method 的返回类型U,我们希望U从可用方法列表中删除以防止调用它两次。这是使用Exclude类型完成的。U在我们的例子中,我们从键的联合中删除K。
因为这种递归类型一直持续到所有方法都被删除为止,所以K我们还需要一个基本情况。这就是这里的条件类型的用途。我们检查剩余的键 ( Exclude<K, U>) 是否扩展never,这本质上意味着是否为“空”。如果是这种情况,请使用方法返回一个构建器build。
最后,唯一剩下的就是builder函数:
function builder(): Builder<Exclude<keyof ConcreteBuilder, 'build'>> {
return new ConcreteBuilder() as any;
}
Run Code Online (Sandbox Code Playgroud)
它返回 aBuilder以及除 之外的所有方法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
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
2103 次 |
| 最近记录: |