打字稿递归方法调用省略属性和方法

Typescript recursive method call to Omit property and method

我有这样的规格

type SpecificType<U, T extends keyof U> = {
    Name: T,
    Value: U[T]
}

class Builder<TItems> {
    private items: SpecificType<TItems, any>[] = [];

    and<TType extends keyof TItems>(type: TType, value: TItems[TType]): Builder<Omit<TItems, TType>> {
        return this.append(this.create(type, value));
    }

    append<TType extends keyof TItems>(item: SpecificType<TItems, TType>): Builder<Omit<TItems, TType>> {
        this.items.push(item);
        return this as Builder<Omit<TItems, TType>>;
    }

    build(): SpecificType<any, any>[] {
        return this.items;
    }

    private create<TType extends keyof TItems>(type: TType, value: TItems[TType]): SpecificType<TItems, TType> {
        return {
            Name: type,
            Value: value
        }
    }
}

Builder 正在基于通用类型构建项目,它基本上是名称和类型之间的映射,如下所示:

type Types = {
    "Type1": undefined,
    "Type2": { MaxAge: number }
}

这是我使用构建器的方式

const itemBuilder = new Builder<Types>();
itemBuilder
    .and("Type1", undefined)
    .and("Type2", { MaxAge: 3 });

我有 2 个问题

  1. 我省略了每次调用 andappend 时的特定类型映射,因为这些方法只能在 TItems 中的每个特定 key 调用一次(或在这种情况下 Types)。当所有 TItems 键都已使用时,是否还可以删除 andappend?目前,如果我在 itemBuilder 上调用另一个 and,自动完成将提供:and(type: never, value: never)。如果在前 2 次调用后 itemBuilder 的自动完成中没有 andappend 会好得多。
  2. 如果您首先查看 anditemBuilder 的调用,因为 Type1 映射是 undefined,我必须将 undefined 作为值传递。如果它没有映射,是否可以以某种方式仅将名称传递给 and 方法 - 因为结果 SpecificType 只是 { Name: string } 而我不需要 value (或我可以在不使用 undefined 的情况下以某种方式声明映射不同??)

首先,我要切换到类型参数是一个或两个大写字母的命名约定,其中 TU 表示一般类型,KP 键类型。无论出于何种原因,这对于 TypeScript 来说都是更标准的。

这是我的做法。对于问题 1,我将创建 NextBuilder 类型别名,如下所示:

type NextBuilder<T, K extends keyof T> = keyof T extends K
  ? Omit<Builder<{}>, "and" | "append">
  : Builder<Omit<T, K>>;

这是一个 conditional type,它检查 keyof T 是否等于(或小于)K。如果是这样,那意味着 Omit<T, K> 将是 {},这就是我们正在尝试处理的情况。在这种情况下,我们将 return Omit<Builder<{}>, "and" | "append">,它会根据需要隐藏 and()append() 方法。否则,Omit<T, K> 仍将具有一些已知属性,我们将像您的原始代码那样 return Builder<Omit<T, K>>。然后我们将 add()append() 更改为 return NextBuilder<T, K> (并使用一些明智的类型断言来安抚编译器,编译器通常无法验证对未解析条件类型的可分配性),并且我们'重做。例如,这里是新的 append():

  append<K extends keyof T>(item: SpecificType<T, K>): NextBuilder<T, K> {
    this.items.push(item);
    return this as any;
  }

对于问题 2,我会更改 and() 的签名,以便在 undefined 是 属性 的可能值的情况下第二个参数是可选的。 (所以它也应该允许你离开 value 如果你有 type Types = {Type3?: string}。这个变化有点奇怪,我不得不跳过几个箍让它在调用站点表现得很好。这是新的 and():

  and<K extends keyof T>(
    type: K,
    ...[value]: undefined extends T[K]
      ? Parameters<(value?: T[K]) => void>
      : Parameters<(value: T[K]) => void>
  ): NextBuilder<T, K> {
    return this.append(this.create(type, value!)) as any;
  }

好的,所以第一个参数就是 type: K。让我们来看第二个。它以 ...[value] 开头,这意味着我们正在使用 rest parameter array and immediately destructuring it into value. After that comes the rest parameter's type annotation, :. The type of the rest parameter array is a conditional type, whose condition is undefined extends T[K] ?. If undefined extends T[K] is true, then the return type is Parameters<(value?: T[K]) => void>, which uses the Parameters utility type to get an optional one-tuple of type [T[K]?]. Otherwise the return type is Parameters<(value: T[K]) => void>, which is of type [T[K]]. Frankly that's a lot more complex than just undefined extends T[K] ? [T[K]?] : [T[K]], but it has an advantage over the former in that it gives the argument the name value in the IntelliSense, and not just something made up like arg0 (there is a bit of documentation,它提到如果您将参数从函数类型推断为元组,然后将其放回函数参数中,则原始参数名称将被保留)。

请注意,在实现中我不得不在 value 上使用 non-null assertion operator (!),因为编译器无法验证 T[K] | undefined 是 [=54= 可接受的第二个参数].非空断言是说 value 实际上是 T[K] 的一种简短方式(我们知道它是,因为只有 undefined 如果 undefined extends T[K] 为真)。

哇!就是这样了。


让我们退后一步,看看它是如何工作的:

const i1 = itemBuilder.and("Type1"); // IntelliSense shows:
// (method) Builder<Types>.and<"Type1">(
//   type: "Type1", value?: undefined
// ): Builder<Pick<Types, "Type2">>
const i2 = i1.and("Type2", { MaxAge: 3 }); // IntelliSense shows:
// (method) Builder<Pick<Types, "Type2">>.and<"Type2">(
//   type: "Type2", value: { MaxAge: number; }
// ): Pick<Builder<{}>, "build">
const i3 = i2.build(); // IntelliSense shows:
// (method) build(): SpecificType<any, any>[]

我觉得这很好。当您第一次使用 and() 时,系统会提示您输入 "Type1""Type2" 作为第一个参数。选择 "Type1" 后,IntelliSense 会显示 value 是可选的第二个参数,您可以将其省略。接下来的and()只允许选择"Type2"value是必须的第二个参数。最后,在此之后,您只能调用 build(),因为缺少 add()append()

我想这给了你想要的东西。


好的,希望对您有所帮助。祝你好运!

Link to code