4

Is there a way to define a Typescript interface which allows one of 2 optional keys in an object or none of them, but not both?

Here is a simplified example of what I'm trying to achieve:

const example1 = { foo: 'some string' }; //should pass - can have only "foo: string"

const example2 = { bar: 42 }; // should pass - can have only "bar: number"

const example3 = {}; // should pass - both foo and bar are optional

const example4 = { foo: 'some string', bar: 42 }; // should throw a Typescript error - can't have both foo and bar simultaneously;

PS. solution should be an interface and not a type since in my use-case it extends another interface

itaydafna
  • 1,968
  • 1
  • 13
  • 26
  • 2
    There is no way to do this relying only on Typescript's `Interface`s. – Fahd Lihidheb Mar 02 '20 at 13:31
  • 1
    It shouldn't, there is no sound way of enforcing that on call point / caller point. Why not use 2 different interfaces? Sounds a lot like they are two different concepts. – Qortex Mar 02 '20 at 13:31
  • @Mic - these are 2 keys in a options object passed to a function - both are valid - but they just can't co-exist – itaydafna Mar 02 '20 at 13:36
  • 1
    Does this answer your question? [typescript interface require one of two properties to exist](https://stackoverflow.com/questions/40510611/typescript-interface-require-one-of-two-properties-to-exist) – Roberto Zvjerković Mar 02 '20 at 13:43

1 Answers1

6

type is isomorphic to interface and you can join these two constructs. Below implementation by type union and intersection, also added example interface to show it works well, & is equivalent to extends. Consider:

interface OtherInterface {
    field?: string
}
type Example = ({
    foo: string
    bar?: never
} | {
    foo?: never
    bar: number
} | {
    [K in any]: never
}) & OtherInterface

const example1: Example = { foo: 'some string' }; // ok

const example2: Example = { bar: 42 }; // ok

const example3: Example = {}; // ok

const example4: Example = { foo: 'some string', bar: 42 }; // error

The solution is verbose, but match your need. Some explanations:

  • bar?: never is used in order to block possibility to have a value which has such field
  • & OtherInterface has exactly the same result as extends OtherInterface
  • {[K in any]: never} - represents empty object {}

Playground link

Maciej Sikora
  • 19,374
  • 4
  • 49
  • 50
  • 1
    Sorry by mistake wrote interface as `type`, fixed now – Maciej Sikora Mar 02 '20 at 13:48
  • Heya, found an error with this and was hoping you could help out, if you have `{ field: 'test' }` without `foo` or `bar` it errors, ideally an empty object would pass but so would one with `field` and no other values. Link to TS playground too long to post so will add additional comment. – Otis Wright Apr 28 '21 at 20:33
  • https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgPJgBbQJLmvJZAbwFgAoZS5GYCAGwBMB+ALmQGcwpQBzcgX3JgAngAcUAUQAecALai6KALzIAFKQpUYAe21tO3EH02UARnCitkICADdoA5AB9i5KtV1Wb9qG6rmoNhAAV1lTBzJ+Z1cTZABtAGlkUGQ4EGEAXSC7CP4ASmQAMjRMHDxYRAhycgRtEE5kCBl5RQBGNmk5BWViDz1kAHJ2bVkUA14B5H4AbmQAejnkbQBrarJa+rBG5u6AJg6dxWQVImQAtgAWXanZhaXVshq6hqauxQBmA7ee05n5xZWaw2L0OEAuXxaPz6bCGIzGXAmABozhZLtc-ndoFBtL5HutnltXpCAKwQ7rHXo0egMGGQTgDZHnZBXG7-e5Agnbb4ANjJRxO1FojFpEHprMxUGxuPIQA – Otis Wright Apr 28 '21 at 20:33