【问题标题】:Enforce Typescript object has exactly one key from a set强制 Typescript 对象只有一组中的一个键
【发布时间】:2022-04-19 03:24:07
【问题描述】:

基本问题和背景

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

const foo = [
  { a: 'foo' },
  { b: 'bar' },
  { c: 'baz' },
]

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

type Foo = { [key in 'a' | 'b' | 'c']: string }[]
const foo: Foo = [
  { a: 'foo' },
  { b: 'bar' },
  { c: 'baz' },
]

这不起作用,因为 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' },
]

我的第二次尝试是:

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' },
]

但是,作为jcalz points out,仍然允许:

const foo: Foo = [{ a: 'foo', b: 'bar' }]

有没有办法强制每个对象只有一个键并且该键是a b c

更多上下文

我们的项目正在尝试读取this JSON 来处理 React 中不同国家的地址字段的动态表单。当 Typescript 读取 JSON blob 时,大多数事情都会出错。最重要的是,它认为 fields 键并不总是一个数组,因此不会让我 .map 超过它。所以我决定将 JSON blob 复制到我们的项目中并手动输入。我试图捕捉fields 数组是thoroughfarepremiselocality 的对象数组,而localitylocalityname 的对象数组等。

【问题讨论】:

  • 您的解决方案也允许const foo: Foo = [{a: "foo", b: "bar"}]。当您说“确切地”一个键时,您的意思是“至少”吗?也就是说,您的解决方案等同于this,这可能是也可能不是您的问题的答案,具体取决于您的要求。
  • 伟大的收获和伟大的问题!我的数据来自一个静态 JSON 文件,其中没有 { a: '', b: '' } 项。话虽如此,我不确定我的打字是否需要排除这种情况。基本上我使用的是if ('a' in obj) { ... },所以在{ a: '', b: '' } 的情况下,我的解决方案取决于顺序但不会中断,因为源数据没有这种情况。话虽如此,让我们假设我不想允许{ a: '', b: '' }。我会更新问题:-)

标签: typescript


【解决方案1】:

如果你想要一个类型,恰好需要一个键,你可以(大部分)将其表示为对象类型的联合,其中联合的每个成员都定义了一个键,其余的键作为可选和the never type。 (实际上,这也将允许undefined,请参阅ms/TS#13195,除非您使用不属于--strict 套件的the --exactOptionalPropertyTypes compiler option

所以你的Foo 应该是这样的:

type Foo = Array<
  { a: string; b?: never; c?: never; } | 
  { a?: never; b: string; c?: never; } |
  { a?: never; b?: never; c: string; }
>

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

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>>;

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

看起来不错。

Playground link to code

【讨论】:

    【解决方案2】:

    this answer 适合您吗?

    type OneKey<K extends string, V = any> = {
        [P in K]: (Record<P, V> &
            Partial<Record<Exclude<K, P>, never>>) extends infer O
            ? { [Q in keyof O]: O[Q] }
            : never
    }[K]; //CREDITS TO JCALZ
    
    type Foo = Array<OneKey<'a' | 'b' | 'c', string>>;
    
    const foo: Foo = [
        { a: 'foo' },
        { b: 'bar' },
        { c: 'baz' },
    ]; //OK
    
    const foo2: Foo = [
        { a: 'foo', b: 'bar', c: 'baz' },
        { a: 'foo', b: 'bar', c: 'baz' },
        { a: 'foo', b: 'bar', c: 'baz' },
    ]; //NOPE
    
    const foo3: Foo = [{ a: 'foo', b: 'bar' }]; //NOPE
    

    【讨论】:

    • 太棒了!我相信这也是正确的,我认为这也使我的问题重复了!但是你给我带来了困难:-) 因为你已经链接到 jcalz 的答案,他 also 直接发布了这个问题的答案,我不确定将哪个标记为回答或是否/如何将此问题标记为重复。我想知道@jcalz 是否可以帮助我确定他们的答案和你的答案是否相同,以及这个问题是否应该被标记为重复?
    • @MarkLodato 我的错,在这种情况下我的回答很愚蠢。当然接受他:)
    猜你喜欢
    • 1970-01-01
    • 2019-02-09
    • 2022-11-19
    • 2019-07-06
    • 2021-08-22
    • 2023-03-27
    • 2018-09-12
    • 2017-09-01
    • 2019-07-02
    相关资源
    最近更新 更多