237

Possibly an odd question, but I'm curious if it's possible to make an interface where one property or the other is required.

So, for example...

interface Message {
    text: string;
    attachment: Attachment;
    timestamp?: number;
    // ...etc
}

interface Attachment {...}

In the above case, I'd like to make sure that either text or attachment exists.


This is how I'm doing it right now. Thought it was a bit verbose (typing botkit for slack).

interface Message {
    type?: string;
    channel?: string;
    user?: string;
    text?: string;
    attachments?: Slack.Attachment[];
    ts?: string;
    team?: string;
    event?: string;
    match?: [string, {index: number}, {input: string}];
}

interface AttachmentMessageNoContext extends Message {
    channel: string;
    attachments: Slack.Attachment[];
}

interface TextMessageNoContext extends Message {
    channel: string;
    text: string;
}
Rafael Tavares
  • 5,678
  • 4
  • 32
  • 48
dsifford
  • 2,809
  • 2
  • 15
  • 16

14 Answers14

257

If you're truly after "one property or the other" and not both you can use never in the extending type:

interface MessageBasics {
  timestamp?: number;
  /* more general properties here */
}
interface MessageWithText extends MessageBasics {
  text: string;
  attachment?: never;
}
interface MessageWithAttachment extends MessageBasics {
  text?: never;
  attachment: string;
}
type Message = MessageWithText | MessageWithAttachment;

//  OK 
let foo: Message = {attachment: 'a'}

//  OK
let bar: Message = {text: 'b'}

// ❌ ERROR: Type '{ attachment: string; text: string; }' is not assignable to type 'Message'.
let baz: Message = {attachment: 'a', text: 'b'}

Example in Playground

robstarbuck
  • 6,893
  • 2
  • 41
  • 40
150

You can use a union type to do this:

interface MessageBasics {
  timestamp?: number;
  /* more general properties here */
}
interface MessageWithText extends MessageBasics {
  text: string;
}
interface MessageWithAttachment extends MessageBasics {
  attachment: Attachment;
}
type Message = MessageWithText | MessageWithAttachment;

If you want to allow both text and attachment, you would write

type Message = MessageWithText | MessageWithAttachment | (MessageWithText & MessageWithAttachment);
Ryan Cavanaugh
  • 209,514
  • 56
  • 272
  • 235
  • 20
    but this will not allow him to have a message with a text and an attachment – Roberto Jun 07 '16 at 20:11
  • 1
    Thanks for the reply Ryan! That's how I'm actually doing it right now. I added that to my question above. Wasn't sure if there was a cleaner way to go about it. – dsifford Jun 07 '16 at 20:22
  • You'll probably want to add `text?: string` and `attachment?: Attachment` to MessageBasics. Otherwise, you'll have to do use type assertion any time you refer to these properties, e.g. `(myMessage as MessageWithText).text` instead of just `myMessage.text`. – Dan B. Jul 05 '19 at 02:22
  • 9
    @Roberto But this or doesn't exclude! This accept both. – Alessandro_Russo Aug 01 '19 at 10:03
  • 17
    The answer does not work as intended, type `Message` accepts `{text: 'a', attachment: 'b'}` https://www.typescriptlang.org/play/index.html?ssl=13&ssc=1&pln=13&pc=48#code/JYOwLgpgTgZghgYwgAgLIQM4bgcwgITg2AQ2QG8BYAKGWTGAFtMw5GAHAfgC5kQBXRgCNoAbhp0A9ACpkjAPZQUeENDgAbZOyjz20BpmQALaCmmSaAXxqhIsRCnRZcEAOrAwRgCoQAHmGQ-SBAAEzInbDxCYlIKCXog3gwwKFAccWpraltoeCQ0TEi3DyMAQTBWBCNmcED-CFDwwpdokjIqWmQ4CsRqhrAklLSMrLAATz0C5zxkAF4povdPH39kAB8FlyWynqqasAyadQgAmHl5XgiXOYpIf14AcjgHgBou3b7wR+fLIA – golopot Dec 24 '19 at 00:12
  • The question asking why this does not work is here https://stackoverflow.com/questions/59462318/why-does-typescript-allow-a-1-b-2-to-be-assigned-to-type-a-any-b?noredirect=1&lq=1 – golopot Dec 24 '19 at 00:13
  • 11
    That last ` | (MessageWithText & MessageWithAttachment)` doesn't do anything - it behaves exactly the same without it. – mik01aj Apr 08 '20 at 14:56
  • Thanks for the helpful answer. I'm curious if you can look at my similar [playground](https://codesandbox.io/s/t0e1i?file=/index.ts:22-27) (lines 22-27) and see why the type checking doesn't work as I'd expect. What am I missing? – Jeremy Thomerson Nov 20 '20 at 01:54
  • Replying to self: the problem was that I was focused on the `if (loc.url)` truthy case and forgot that `{ url: "" }` would be falsy. @ford04 helped me out in response to a comment on another question [fixed playground](https://codesandbox.io/s/e1omz) – Jeremy Thomerson Nov 20 '20 at 15:13
  • If you want to allow both text and attachment, have `attachment? : never` in `MessageWithText`, `text? : never` in `MessageWithAttachment` and a 3rd type `MessageWithTextAndAttachment` defining both the properties together. And finally, define `type Message = MessageWithText | MessageWithAttachment | MessageWithTextAndAttachment;` – Ritesh Jagga Dec 28 '22 at 17:05
  • this isn't allowing me to destructure one of the required props directly off an object – Daniel Lizik Jan 16 '23 at 11:20
73

You can go deeper with @robstarbuck solution creating the following types:

type Only<T, U> = {
  [P in keyof T]: T[P];
} & {
  [P in keyof U]?: never;
};

type Either<T, U> = Only<T, U> | Only<U, T>;

And then Message type would look like this

interface MessageBasics {
  timestamp?: number;
  /* more general properties here */
}
interface MessageWithText extends MessageBasics {
  text: string;
}
interface MessageWithAttachment extends MessageBasics {
  attachment: string;
}
type Message = Either<MessageWithText, MessageWithAttachment>;

With this solution you can easily add more fields in MessageWithText or MessageWithAttachment types without excluding it in another.

Voskanyan David
  • 987
  • 6
  • 6
  • 7
    Great generic solution. In case if you have interfaces where general properties are identical in key but have certain different values you can use this modified version of `Only`: `type Only = { [P in keyof T]: T[P] } & Omit<{ [P in keyof U]?: never }, keyof T>` – ogostos Oct 25 '21 at 15:04
  • 2
    This is a life saving solution. Props to you. I am amazed that Typescript has no option to do this more easily. – Megajin Jan 03 '22 at 15:02
  • @ogostos I'm using `export type Only = { [P in keyof T]: P extends keyof U ? never : T[P]; };`, seems less convoluted both on writing and on the structure it generates. – his dudeness Aug 06 '22 at 17:42
  • @hisdudeness [I don't think your solution is working for me](https://www.typescriptlang.org/play?#code/C4TwDgpgBA8gdgGxAHgCoBooFUB8UC8UA3lKQNoAKUAlnFANYQgD2AZlKgLoBcHlnUAL5QAZLAC21YMhLkqtBkzbZOAfl5wIANwgAnIZkYt2qHACgA9BaihIsRCgzY8hEpRp0jyrryoQAHsAQcAAmAM6KxthQqlCaOvq8qPwA3EIpZpbWttDwSGiYuATEWaRQ7gpeJjx8FJwZVkKiJY1yHpHKWGoa2noN1oIZZjlQAKJSABZ6Bc7FeY6FeAA+9vlYmKZDIwCGxURmZVDbvGHAurQA5gdlAEYaAK7iN3pmgpkjN3vXpMdxj8+6b5QO5QU7nOBXN5mADGzDgpyOzCRvHGwCmumQ20wNxcJUOvwA5ABGAnoIEggBMZMEQA). – ogostos Aug 16 '22 at 13:14
  • It works if property names are different. – Ritesh Jagga Dec 29 '22 at 09:35
  • to me, `timestamp` is of type `never` – Greg Wozniak Jul 30 '23 at 13:32
12

I've stumbled upon this thread when looking for an answer for my case (either propA, propB or none of them). Answer by Ryan Fujiwara almost made it but I've lost some checks by that.

My solution:

interface Base {   
  baseProp: string; 
}

interface ComponentWithPropA extends Base {
  propA: string;
  propB?: never;
}

interface ComponentWithPropB extends Base {
  propB: string;
  propA?: never;
} 

interface ComponentWithoutProps extends Base {
  propA?: never;
  propB?: never;
}

type ComponentProps = ComponentWithPropA | ComponentWithPropB | ComponentWithoutProps;

This solution keeps all checks as they should be. Perhaps someone will find this useful :)

Rich
  • 5,603
  • 9
  • 39
  • 61
Darek
  • 191
  • 2
  • 5
9

Thanks @ryan-cavanaugh that put me in the right direction.

I have a similar case, but then with array types. Struggled a bit with the syntax, so I put it here for later reference:

interface BaseRule {
  optionalProp?: number
}

interface RuleA extends BaseRule {
  requiredPropA: string
}

interface RuleB extends BaseRule {
  requiredPropB: string
}

type SpecialRules = Array<RuleA | RuleB>

// or

type SpecialRules = (RuleA | RuleB)[]

// or (in the strict linted project I'm in):

type SpecialRule = RuleA | RuleB
type SpecialRules = SpecialRule[]

Update:

Note that later on, you might still get warnings as you use the declared variable in your code. You can then use the (variable as type) syntax. Example:

const myRules: SpecialRules = [
  {
    optionalProp: 123,
    requiredPropA: 'This object is of type RuleA'
  },
  {
    requiredPropB: 'This object is of type RuleB'
  }
]

myRules.map((rule) => {
  if ((rule as RuleA).requiredPropA) {
    // do stuff
  } else {
    // do other stuff
  }
})
publicJorn
  • 2,404
  • 1
  • 20
  • 30
  • 14
    Just a quick note to let you know that there is a type-safe, intellisense-friendly way to write your `.map()` code. Use type-guard with the `in` operator. TypeScript will do proper type inference, etc. So your condition would be `if('requiredPropA' in rule) { /* inside here, TS knows rule is of type */ }` – Eric Liprandi Nov 02 '18 at 17:20
9

Simple 'need one of two' example:

type Props =
  | { factor: Factor; ratings?: never }
  | { ratings: Rating[]; factor?: never }
Shah
  • 2,126
  • 1
  • 16
  • 21
8

You can create few interfaces for the required conditions and join them in a type like here:

interface SolidPart {
    name: string;
    surname: string;
    action: 'add' | 'edit' | 'delete';
    id?: number;
}
interface WithId {
    action: 'edit' | 'delete';
    id: number;
}
interface WithoutId {
    action: 'add';
    id?: number;
}

export type Entity = SolidPart & (WithId | WithoutId);

const item: Entity = { // valid
    name: 'John',
    surname: 'Doe',
    action: 'add'
}
const item: Entity = { // not valid, id required for action === 'edit'
    name: 'John',
    surname: 'Doe',
    action: 'edit'
}
cuddlemeister
  • 1,586
  • 12
  • 15
8

I have further compressed the solution from @Voskanyan David to get to this solution which I personally find very neat:

You still have to define Only<> and Either<> once somewhere

type Only<T, U> = {
    [P in keyof T]: T[P];
} & {
    [P in keyof U]?: never;
};

type Either<T, U> = Only<T, U> | Only<U, T>

Afterwards, it becomes possible to define it without any intermediate interfaces/types as one thing:

type Message {
    type?: string;
    channel?: string;
    user?: string;
    // ...etc
} & Either<{message: string}, {attachment: Attachment}>
Fabian Zimbalev
  • 493
  • 4
  • 12
6

You can also use an abstract class for the general properties instead of an interface, to prevent someone from accidentally implementing that interface.

abstract class BaseMessage {
  timestamp?: number;
  /* more general properties here */
  constructor(timestamp?: number) {
    this.timestamp = timestamp;
    /* etc. for other general properties */
  }
}
interface IMessageWithText extends BaseMessage {
  text: string;
  attachment?: never;
}
interface IMessageWithAttachment extends BaseMessage {
  text?: never;
  attachment: string;
}
type Message = IMessageWithText | IMessageWithAttachment;
Edvin Larsson
  • 95
  • 1
  • 3
  • 11
3

There're some cool Typescript option that you could use https://www.typescriptlang.org/docs/handbook/utility-types.html#omittk

Your question is: make an interface where either 'text' or attachment exist. You could do something like:

interface AllMessageProperties {
  text: string,
  attachement: string,
}

type Message = Omit<AllMessageProperties, 'text'> | Omit<AllMessageProperties, 'attachement'>;

const messageWithText : Message = {
  text: 'some text'
}

const messageWithAttachement : Message = {
  attachement: 'path-to/attachment'
}

const messageWithTextAndAttachement : Message = {
  text: 'some text',
  attachement: 'path-to/attachment'
}

// results in Typescript error
const messageWithOutTextOrAttachement : Message = {

}
Stefan van de Vooren
  • 2,524
  • 22
  • 19
  • 1
    Is there a way to make this `text` XOR `attachment` using `Omit`? – Chris R Nov 04 '20 at 12:01
  • Any way to make this compatible with parameter destructuring? I get TS errors if I try this: – CletusW Nov 19 '20 at 23:27
  • Any way to make this compatible with parameter destructuring? I get TS errors if I try `function myFunc({ text, attachment }: Message) {}` – CletusW Nov 19 '20 at 23:35
3

Ok, so after while of trial and error and googling I found that the answer didn't work as expected for my use case. So in case someone else is having this same problem I thought I'd share how I got it working. My interface was such:

export interface MainProps {
  prop1?: string;
  prop2?: string;
  prop3: string;
}

What I was looking for was a type definition that would say that we could have neither prop1 nor prop2 defined. We could have prop1 defined but not prop2. And finally have prop2 defined but not prop1. Here is what I found to be the solution.

interface MainBase {
  prop3: string;
}

interface MainWithProp1 {
  prop1: string;
}

interface MainWithProp2 {
  prop2: string;
}

export type MainProps = MainBase | (MainBase & MainWithProp1) | (MainBase & MainWithProp2);

This worked perfect, except one caveat was that when I tried to reference either prop1 or prop2 in another file I kept getting a property does not exist TS error. Here is how I was able to get around that:

import {MainProps} from 'location/MainProps';

const namedFunction = (props: MainProps) => {
    if('prop1' in props){
      doSomethingWith(props.prop1);
    } else if ('prop2' in props){
      doSomethingWith(props.prop2);
    } else {
      // neither prop1 nor prop2 are defined
    }
 }

Just thought I'd share that, cause if I was running into that little bit of weirdness then someone else probably was too.

RyanFuji
  • 39
  • 3
  • Property 'prop1' does not exist on type 'MainProps'. Property 'prop2' does not exist on type 'MainProps'. https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgLJ1AITgZxQbwFgAoZZABygHtyBmALmRzClAHMBuEgXxJNEixEKdKADqwMAAsACtXIBGZEVIV5Cxs1YhOPPsQHR4SNBhATpcmgCZlJMpRuaW7LsV7ESEAB7kqUMGQwAE9yETMrchxkAF5TLFwUAB9kAApREGw8ZAAyePNJWXUASmQU9LMslDyMiyKbYrcSBCoQZmQQOABbCAATADEAVxAEMGBW2LT8RzoAGhmFWbUbZG5GDMicUpiAPmVuIA – Jalal Jun 29 '21 at 13:44
  • @Jalal the "Elvis" operator, "?" (at least that's what I was told what it was called) is probably missing from your exported interface. This says that `prop1` and `prop2` aren't required. – RyanFuji Apr 09 '22 at 17:39
3

Nobody has mentioned it so far, but I think that whoever stumbles upon this page may also consider using discriminated unions. If I properly understood the intensions of the OP's code then it might be transformed like this.

interface Attachment {}

interface MessageBase {
    type?: string;
    user?: string;
    ts?: string;
    team?: string;
    event?: string;
    match?: [string, {index: number}, {input: string}];
}

interface AttachmentMessageNoContext extends MessageBase {
    kind: 'withAttachments',
    channel: string;
    attachments: Attachment[];
}

interface TextMessageNoContext extends MessageBase {
    kind: 'justText', 
    channel: string;
    text: string;
}

type Message = TextMessageNoContext | AttachmentMessageNoContext

const textMessage: Message = {
  kind: 'justText',
  channel: 'foo',
  text: "whats up???" 
}

const messageWithAttachment: Message = {
  kind: 'withAttachments',
  channel: 'foo',
  attachments: []
}

Now Message interface requires either attachments or text depending on the kind property.

ansavchenco
  • 535
  • 6
  • 14
2

Without using extension

Using XOR described here: https://stackoverflow.com/a/53229567/8954109

// Create a type that requires properties `a` and `z`, and one of `b` or `c`
type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
type XOR<T, U> = (T | U) extends object ? (Without<T, U> & U) | (Without<U, T> & T) : T | U;

interface Az {
  a: number;
  z: number;
}

interface B {
  b: number;
}

interface C {
  c: number;
}

type XorBC = XOR<B, C>;
type AndAzXorBC = Az & XorBC;
type MyData = AndAzXorBC;

const ok1: MyData = { a: 0, z: 1, b: 2 };
const ok2: MyData = { a: 0, z: 1, c: 2 };
const badBothBC: MyData = {
  a: 0, z: 1,
  b: 2,
  c: 3
};
const badNoBC: MyData = { a: 0, z: 1 };
const badNoZ: MyData = { a: 0, b: 2 };

Produces these errors for invalid types:

src/App.tsx:30:7 - error TS2322: Type '{ a: number; z: number; b: number; c: number; }' is not assignable to type 'AndAzXorBC'.
  Type '{ a: number; z: number; b: number; c: number; }' is not assignable to type 'Az & Without<C, B> & B'.
    Type '{ a: number; z: number; b: number; c: number; }' is not assignable to type 'Without<C, B>'.
      Types of property 'c' are incompatible.
        Type 'number' is not assignable to type 'undefined'.

30 const badBothBC: MyData = {
src/App.tsx:35:7 - error TS2322: Type '{ a: number; z: number; }' is not assignable to type 'AndAzXorBC'.
  Type '{ a: number; z: number; }' is not assignable to type 'Az & Without<C, B> & B'.
    Property 'b' is missing in type '{ a: number; z: number; }' but required in type 'B'.

35 const badNoBC: MyData = { a: 0, z: 1 };
         ~~~~~~~

  src/App.tsx:17:3
    17   b: number;
         ~
    'b' is declared here.
src/App.tsx:36:7 - error TS2322: Type '{ a: number; b: number; }' is not assignable to type 'AndAzXorBC'.
  Type '{ a: number; b: number; }' is not assignable to type 'Az & Without<C, B> & B'.
    Property 'z' is missing in type '{ a: number; b: number; }' but required in type 'Az'.

36 const badNoZ: MyData = { a: 0, b: 2 };
         ~~~~~~

  src/App.tsx:13:3
    13   z: number;
         ~
    'z' is declared here.
plswork04
  • 589
  • 6
  • 11
0

Here is a very simple utility type that I use for creating a new type that allows one of multiple interfaces/types called InterfacesUnion:

export type InterfacesUnion<Interfaces> = BuildUniqueInterfaces<UnionToIntersection<Interfaces>, Interfaces>;

type UnionToIntersection<U> = (U extends unknown ? (k: U) => void : never) extends (k: infer I) => void ? I : never;

export type BuildUniqueInterfaces<CompleteInterface, Interfaces> = Interfaces extends object
  ? AssignNever<CompleteInterface, Interfaces>
  : never;

type AssignNever<T, K> = K & {[B in Exclude<keyof T, keyof K>]?: never};

It can be used as follows:

type NewType = InterfacesUnion<AttachmentMessageNoContext | TextMessageNoContext>

It operates by accepting a union of interfaces/types, building up a single interface that contains all of their properties and returning the same union of interfaces/types with each one containing additional properties that other interfaces had and they didn't with those properties being set to optional never ([propertyName]?: never).

Playground with examples/explanation

Ovidijus Parsiunas
  • 2,512
  • 2
  • 8
  • 18