首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >使用TypeScript类型系统创建一个流畅、有状态的生成器

使用TypeScript类型系统创建一个流畅、有状态的生成器
EN

Stack Overflow用户
提问于 2019-09-06 22:01:01
回答 1查看 1.4K关注 0票数 4

我想要创建一个生成器,它可以表示builder.a().b().build()builder.b().a().build(),但不能代表builder.a().build()builder.b().build()builder.a().a().build()等。

显然,我可以在build方法中进行验证,但是我希望编译器能够提示这一点(并且vs代码提供自动完成)。我认为TS类型系统可以使用映射类型、联合和交叉点来表示这个类型,但我无法完全理解它。

有人知道我是怎么做到的吗?

EN

回答 1

Stack Overflow用户

回答已采纳

发布于 2019-09-07 07:39:36

让我们从最简单的部分开始,实际的实现。该类使用this作为返回类型。实际的魔术将在以后使用另一种类型发生:

代码语言:javascript
复制
class ConcreteBuilder {
  a(): this {
    return this;
  }

  b(): this {
    return this;
  }

  build(): string {
    return 'foo';
  }
}

接下来,我们需要一个泛型类型,在那里我们可以传递一个函数类型,它将返回类型交换为其他类型。你马上就会明白为什么。我从另一个答案复制了这段代码

代码语言:javascript
复制
type ArgumentTypes<T> = T extends (... args: infer U) => infer R ? U: never;
type ReplaceReturnType<O, N> = (...a: ArgumentTypes<O>) => N;

现在它变得有趣了,下面是我提出的Builder类型:

代码语言:javascript
复制
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保留原始参数,但将返回类型替换为:

代码语言:javascript
复制
Builder<Exclude<K, U> extends never ? 'build' : Exclude<K, U>>

罗得要在这里打开行李。因此,正如您所看到的,我们将返回类型替换为Builder。讨论过的参数定义了哪些方法是可用的。由于这是方法U的返回类型,因此我们希望从可用方法列表中删除U,以防止再次调用它。这是使用类型完成的。在我们的例子中,我们将U从键K的联合中删除。

因为这种递归类型一直持续到从K中删除所有方法,所以我们还需要一个基本情况。这就是条件类型的作用所在。我们检查剩余的键(Exclude<K, U>)是否扩展了never,这实质上意味着是否为“空”。如果是这样的话,返回一个带build方法的生成器。

最后,只剩下builder函数了:

代码语言:javascript
复制
function builder(): Builder<Exclude<keyof ConcreteBuilder, 'build'>> {
  return new ConcreteBuilder() as any;
}

它返回一个Builder,其中包含除build之外的所有方法。有一个any强制转换是必要的,因为ConcreteBuilder的类型比Builder的限制要小。

全码

代码语言:javascript
复制
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

游乐场

票数 7
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/57828972

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档