Home > other >  Typescript require one of two properties to exist with nested property
Typescript require one of two properties to exist with nested property

Time:07-28

This question is similar to this, but the key difference is that I want to use nested attribute:

Imagine following TS interfaces:

export interface Nested {
  a?: string;
  b?: string;
}

export interface Parent {
  nested?: Nested;
  c?: string;
}

I want to enforce that exactly one of c or nested.a exists. Is there a way to achieve this in TypeScript?

CodePudding user response:

In order for this to work, Parent must be a union representing the different ways to meet your constraint. Essentially either nested.a is present and c is absent, or nested.a is absent and c is present. For a property to be absent you can make it optional and have its property value type be the never type (this often allows the undefined type in there but that's hopefully not an issue). For a property to be present you should make it (and any parent properties) required.

For your example code that looks like

type Parent =
  { nested: { a: string, b?: string }, c?: never } |
  { nested?: { a?: never, b?: string }, c: string }

And you can verify that it works as desired:

let p: Parent;
p = { nested: { b: "abc" }, c: "def" } // okay
p = { nested: { a: "abc", b: "def" } } // okay
p = { nested: { b: "abc" } } // error
p = { nested: { a: "abc", b: "def" }, c: "ghi" } // error
p = { nested: {}, c: "abc" } // okay
p = { c: "abc" } // okay

Note that I didn't reuse your original Nested definition. If you really need to you can use it along with utility types like Pick/Omit and Partial/Required:

type Parent =
  { nested: Required<Pick<Nested, "a">> & Omit<Nested, "a">, c?: never } |
  { nested?: { a?: never } & Omit<Nested, "a">, c: string }

But this only really makes sense if Nested has a lot of properties in it, and even then you might want to refactor:

interface BaseNested {
  b?: string
  // other props
}
interface NestedWithA extends BaseNested {
  a: string
}
interface NestedWithoutA extends BaseNested {
  a?: never
}
type Parent =
  { nested: NestedWithA, c?: never } |
  { nested?: NestedWithoutA, c: string }

Playground link to code

  • Related