强制 Typescript 对象只有一组键中的一个

Mar*_*ato 11 typescript

基本问题和背景

我正在尝试输入一个对象数组,其中每个对象都具有一组中的一个键。例如:

const foo = [
  { a: 'foo' },
  { b: 'bar' },
  { c: 'baz' },
]
Run Code Online (Sandbox Code Playgroud)

我的第一次尝试是key in联合:

type Foo = { [key in 'a' | 'b' | 'c']: string }[]
const foo: Foo = [
  { a: 'foo' },
  { b: 'bar' },
  { c: 'baz' },
]
Run Code Online (Sandbox Code Playgroud)

这不起作用,因为 Typescript 希望每个对象都拥有联合中的所有键:

type Foo = { [key in 'a' | 'b' | 'c']: string }[]
const foo: Foo = [
  { a: 'foo', b: 'bar', c: 'baz' },
  { a: 'foo', b: 'bar', c: 'baz' },
  { a: 'foo', b: 'bar', c: 'baz' },
]
Run Code Online (Sandbox Code Playgroud)

我的第二次尝试是:

type A = { a: string }
type B = { b: string }
type C = { c: string }
type Foo = (A | B | C)[]
const foo: Foo = [
  { a: 'foo' },
  { b: 'bar' },
  { c: 'baz' },
]
Run Code Online (Sandbox Code Playgroud)

但是,正如jcalz 指出的那样,这仍然允许:

const foo: Foo = [{ a: 'foo', b: 'bar' }]
Run Code Online (Sandbox Code Playgroud)

有没有一种方法可以强制每个对象都只有一个键,并且该键是a or b c

稍微多一点上下文

我们的项目正在尝试读取此 JSON,以处理 React 中不同国家/地区地址字段的动态表单。当 Typescript 读取 JSON blob 时,大多数情况都会出错。最重要的是,它认为fields密钥并不总是数组,因此不会让我.map忽略它。因此,我决定将 JSON blob 复制到我们的项目中并手动输入。我试图捕捉这样一个事实:该fields数组是一个对象数组,这些对象是thoroughfarepremise、 或 ,locality而这locality是一个对象数组localityname,这些对象是 等。

jca*_*alz 8

如果您想要一种只需要一个键的类型,则可以(大多数情况下)将其表示为对象类型的联合,其中联合的每个成员都定义了一个键,而所有其余键都是可选的且类型never。(实际上,这也允许undefined,请参阅ms/TS#13195,除非您使用不属于该套件的编译器选项--exactOptionalPropertyTypes--strict

所以你的Foo应该看起来像这样:

type Foo = Array<
  { a: string; b?: never; c?: never; } | 
  { a?: never; b: string; c?: never; } |
  { a?: never; b?: never; c: string; }
>
Run Code Online (Sandbox Code Playgroud)

我们如何以编程方式获得它或类似的东西?解释起来有点棘手,但我的解决方案如下所示:

type ExactlyOneKey<K extends keyof any, V, KK extends keyof any = K> =
  { [P in K]: { [Q in P]: V } &
    { [Q in Exclude<KK, P>]?: never} extends infer O ?
    { [Q in keyof O]: O[Q] } : never
  }[K];

type Foo = Array<ExactlyOneKey<"a" | "b" | "c", string>>;
Run Code Online (Sandbox Code Playgroud)

该类型ExactlyOneKey<K, V>采用键联合K并对其进行迭代。P对于联合的每个成员,它都会创建一个对象类型,其中存在该键,而其他键不存在/丢失。类型{[Q in P]: V}(又名Record<P, V>)具有当前的键和值,并且该类型{[Q in Exclude<KK, P>]?: never}具有所有其余键作为可选且从不。我们将它们与 相交&以获得具有这两种特征的类型。然后我做了一个小技巧,... extends infer O ? { [Q in keyof O]: O[Q] } : never将获取该类型...并将所有交叉点合并为单个对象类型。这并不是绝对必要的,但它会改变{a: string} & {b?: never, c?: never}{a: string; b?: never; c?: never;}更容易接受的。

让我们确保它有效:

const foo: Foo = [
  { a: 'foo' },
  { b: 'bar' },
  { c: 'baz' },
]; // okay

const badFoo: Foo = [
  { d: "nope" }, // error
  { a: "okay", b: "oops" }  // error
];
Run Code Online (Sandbox Code Playgroud)

看起来不错。

Playground 代码链接