使用 TypeScript 类型系统创建流畅、有状态的构建器

Kir*_*Kir 5 typescript

我想创建一个可以代表builder.a().b().build()or 的构建器builder.b().a().build(),但不能代表builder.a().build(), builder.b().build()orbuilder.a().a().build()等​​。

显然我可以在构建方法中进行验证,但我希望编译器对此进行提示(并让 vs code 提供自动完成功能)。我认为 TS 类型系统可以使用映射类型、并集和交集来表示这一点,但我不太明白。

有谁知道我该怎么做?

luk*_*ter 8

让我们从最简单的部分开始,即实际的实现。该类用作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)

操场