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 }