说我有这种类型:
export interface Opts {
paths?: string | Array<string>,
path?: string | Array<string>
}
我想告诉用户他们必须传递路径或路径,但没有必要通过它们。现在的问题是这个编译:
export const foo = (o: Opts) => {};
foo({});
有谁知道允许2个或更多可选但至少1是TS的必要参数?
你可以用
export type Opts = { path: string | Array<string> } | { paths: string | Array<string> }
为了提高可读性,您可以写:
type StringOrArray = string | Array<string>;
type PathOpts = { path : StringOrArray };
type PathsOpts = { paths: StringOrArray };
export type Opts = PathOpts | PathsOpts;
如果您已经定义了该接口并且希望避免重复声明,则选项可以是创建一个条件类型,该类型接受一个类型并返回一个union,其中包含一个字段的union中的每个类型(以及never
值的记录)任何其他字段,以消除指定的任何额外字段)
export interface Opts {
paths?: string | Array<string>,
path?: string | Array<string>
}
type EitherField<T, TKey extends keyof T = keyof T> =
TKey extends keyof T ? { [P in TKey]-?:T[TKey] } & Partial<Record<Exclude<keyof T, TKey>, never>>: never
export const foo = (o: EitherField<Opts>) => {};
foo({ path : '' });
foo({ paths: '' });
foo({ path : '', paths:'' }); // error
foo({}) // error
编辑
关于这里使用的魔术类型的一些细节。我们将使用distributive property of conditional types实际迭代T
类型的所有键。分配属性需要额外的类型参数才能工作,我们为此目的引入TKey
,但我们还提供所有键的默认值,因为我们想要获取T
类型的所有键。
因此,我们要做的是实际获取原始类型的每个键,并创建一个仅包含该键的新映射类型。结果将是包含单个键的所有映射类型的并集。映射类型将删除属性的可选性(-?
,描述为here),该属性与T
(T[TKey]
)中的原始属性的属性相同。
需要解释的最后一部分是Partial<Record<Exclude<keyof T, TKey>, never>>
。由于对对象文字的多余属性检查有效,我们可以在分配给它的对象键中指定联合的任何字段。对于像{ path: string | Array<string> } | { paths: string | Array<string> }
这样的联盟,我们可以分配这个对象文字{ path: "", paths: ""}
,这是不幸的。解决方案是要求如果T
的任何其他属性(除了TKey
,所以我们得到Exclude<keyof T, TKey>
)存在于任何给定联合成员的对象文字中,它们应该是never
类型(所以我们得到Record<Exclude<keyof T, TKey>, never>>
)。但我们不希望必须为所有成员明确指定never
,这就是为什么我们Partial
以前的记录。
这有效。
它接受一个通用类型T
,在你的情况下是string
。
泛型类型OneOrMore
定义了T
中的1个或T
的数组。
您的通用输入对象类型Opts
要么是path
的密钥OneOrMore<T>
,要么是paths
的密钥OneOrMore<T>
。虽然没有必要,但我明确指出,唯一的其他选择永远不可接受。
type OneOrMore<T> = T | T[];
export type Opts<T> = { path: OneOrMore<T> } | { paths: OneOrMore<T> } | never;
export const foo = (o: Opts<string>) => {};
foo({});
{}
发生错误