javascript - 映射类型,其中一个键是可能键中的必需键
下面的代码允许创建一个对象,该对象包含“k1”或“k2”或(“k1”和“k2”)或都不包含。
type Foo = "k1"|"k2";
type Bar = {[key in Foo]?: string};
const myObj1: Bar = { // ok
}
const myObj3: Bar = { // ok
"k1": "random string",
"k2": "random string",
}
我的目标是能够拥有一个且仅有一个必需的键,要么是“k1”,要么是“k2”。
所以我想能够做到以下几点:
const myObj1: Bar = { // Compile error
}
const myObj2: Bar = { // Compile error
"k1": "random string",
"k2": "random string",
}
const myObj3: Bar = { // ok
"k1": "random string",
}
const myObj4: Bar = { // ok
"k2": "random string",
}
答案#1
为了使其按你希望的方式工作,你需要 Bar 是一个联合类型,其中联合中的每个成员都有一个必需属性,其键来自 Foo,值的类型为 string,而 Foo 中的其余键是被禁止的。 TypeScript 并不真正允许你禁止一个键(至少不启用 --exactOptionalPropertyTypes 编译选项时),但将它们设为 never 类型的可选属性非常接近:例如,{x?: never} 意味着 x 属性要么不存在,要么必须存在且值的类型为 never(这是不可能的)或 undefined(可选属性会自动加上)。无论如何,这意味着对于你问题中的特定 Foo,你希望 Bar 是:
type Bar = {
k1: string;
k2?: never;
} | {
k2: string;
k1?: never;
}
但可以假设你想要从 Foo 计算 Bar,这样如果 Foo 发生变化,Bar 会自动更新以匹配它。这里有一种方法可以做到这一点:
type Bar = { [K in Foo]:
{ [P in K]: string } & { [P in Exclude<Foo, K>]?: never }
}[Foo];
这是使用一个映射类型,并立即对其进行索引以获得一个联合类型。我们可以称之为“分发类型”。任何类似于 {[K in XXX]: YYY<K>}[XXX] 的方法,其中 XXX 是某个联合类型 K1 | K2 | K3,都会创建一个形如 YYY<K1> | YYY<K2> | YYY<K3> 的分发类型。
在上述类型中,我们因此正在为 Foo 中的每个 K 计算 { [P in K]: string } & { [P in Exclude<Foo, K>]?: never } 的并集。所以它将变为 ({ [P in "k1"]: string } & { [P in Exclude<Foo, "k1">]?: never }) | ({ [P in "k2"]: string } & { [P in Exclude<Foo, "k2">]?: never })
让我们只看那部分的开头:
{ [P in "k1"]: string } & { [P in Exclude<Foo, "k1">]?: never }
左半部分等价于 {k1: string}(因为我们只映射一个键),右半部分等价于 {k2?: never},因为 Exclude<T, U> 工具类型会过滤掉联合成员。这两部分相交在一起,所以 {k1: string} & {k2?: never},这等价于 {k1: string, k2?: never}(但不会以这种方式表示)。
所以上述将生成一种类型
/* type Bar = ({
k1: string;
} & {
k2?: never;
}) | ({
k2: string;
} & {
k1?: never;
}) */
它的作用和你想的一样,但有那么多交叉,看起来有点丑。
你可以通过执行一个“无操作”的映射类型,让编译器将对象类型的交集折叠为单一的对象类型。如果 O 是 {a: 1} & {b: 2},那么 {[P in keyof O]: O[P]} 将会是 {a: 1, b: 2}。所以我们可以扩展 Bar 的定义来实现这一点:
type Bar = { [K in Foo]:
{ [P in K]: string } & { [P in Exclude<Foo, K>]?: never } extends
infer O ? { [P in keyof O]: O[P] } : never
}[Foo];
这段代码所做的是将 { [P in K]: string } & { [P in Exclude<Foo, K>]?: never } 复制到一个新的类型参数 O,然后使用 {[P in keyof O]: O[P]} 遍历它以合并交叉类型。哦,我还使用了条件类型推断来完成复制。任何形式为 XXX extends infer O ? YYY<O> : never 的类型都等价于 YYY<XXX>。
所以最后,根据这个定义,我们得到:
/* type Bar = {
k1: string;
k2?: never;
} | {
k2: string;
k1?: never;
} */
让我们确保它按照你的想法工作:
const myObj1: Bar = { // Compile error
}
const myObj2: Bar = { // Compile error
"k1": "random string",
"k2": "random string",
}
const myObj3: Bar = { // ok
"k1": "random string",
}
const myObj4: Bar = { // ok
"k2": "random string",
}
并且确保当你向 Foo 添加一个键时它会更新:
type Foo = "k1" | "k2" | "k3";
/* type Bar = {
k1: string;
k2?: never;
k3?: never;
} | {
k2: string;
k1?: never;
k3?: never;
} | {
k3: string;
k1?: never;
k2?: never;
} */
看起来不错!
代码的 Playground 链接
答案#2
试试这个:
type Bar = {k1: string, k2?: never} | {k1?: never, k2: string};
