打字稿递归方法调用省略属性和方法
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 个问题
- 我省略了每次调用
and
或 append
时的特定类型映射,因为这些方法只能在 TItems
中的每个特定 key
调用一次(或在这种情况下 Types
)。当所有 TItems
键都已使用时,是否还可以删除 and
和 append
?目前,如果我在 itemBuilder
上调用另一个 and
,自动完成将提供:and(type: never, value: never)
。如果在前 2 次调用后 itemBuilder
的自动完成中没有 and
和 append
会好得多。
- 如果您首先查看
and
对 itemBuilder
的调用,因为 Type1
映射是 undefined
,我必须将 undefined
作为值传递。如果它没有映射,是否可以以某种方式仅将名称传递给 and
方法 - 因为结果 SpecificType
只是 { Name: string }
而我不需要 value
(或我可以在不使用 undefined
的情况下以某种方式声明映射不同??)
首先,我要切换到类型参数是一个或两个大写字母的命名约定,其中 T
和 U
表示一般类型,K
和 P
键类型。无论出于何种原因,这对于 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()
。
我想这给了你想要的东西。
好的,希望对您有所帮助。祝你好运!
我有这样的规格
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 个问题
- 我省略了每次调用
and
或append
时的特定类型映射,因为这些方法只能在TItems
中的每个特定key
调用一次(或在这种情况下Types
)。当所有TItems
键都已使用时,是否还可以删除and
和append
?目前,如果我在itemBuilder
上调用另一个and
,自动完成将提供:and(type: never, value: never)
。如果在前 2 次调用后itemBuilder
的自动完成中没有and
和append
会好得多。 - 如果您首先查看
and
对itemBuilder
的调用,因为Type1
映射是undefined
,我必须将undefined
作为值传递。如果它没有映射,是否可以以某种方式仅将名称传递给and
方法 - 因为结果SpecificType
只是{ Name: string }
而我不需要value
(或我可以在不使用undefined
的情况下以某种方式声明映射不同??)
首先,我要切换到类型参数是一个或两个大写字母的命名约定,其中 T
和 U
表示一般类型,K
和 P
键类型。无论出于何种原因,这对于 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()
。
我想这给了你想要的东西。
好的,希望对您有所帮助。祝你好运!