1

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?

Tofig Hasanov
  • 3,303
  • 10
  • 51
  • 81
  • There's numerous answers on this in the very question you linked, what did you try and why didn't it work? – Etheryte Jul 27 '22 at 15:27
  • 1
    Does [this approach](https://tsplay.dev/WPRLKN) meet your needs when tested against your use cases? If so I could write up an answer; if not, what am I missing? – jcalz Jul 27 '22 at 15:29
  • @Etheryte - I tried using `RequireAtLeastOne` approach from one of the answers, but TypeScript doesn't allow me to list nested attributes like `RequireAtLeastOne`. – Tofig Hasanov Jul 27 '22 at 15:30
  • @jcalz - I think that works, thanks! Although I would prefer to reuse defined `Nested` interface, without having to redefine it. I guess it shouldn't be hard though, using Pick/Omit – Tofig Hasanov Jul 27 '22 at 15:32
  • The example is so minimal that `Pick`/`Omit` is definitely not worth it. I could mention in the answer that you could always programmatically generate these types from `Nested` if you want to – jcalz Jul 27 '22 at 15:34

1 Answers1

1

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

jcalz
  • 264,269
  • 27
  • 359
  • 360