6

Is it possible to define interface or a type in TS which will make sure that if prop A is defined prop B must be also (and if A is not defined/set, B mustn't be either). For example I have smth. like:

export interface IExample {
  a: string;
  b: () => void;
  c: boolean;
}

Then if I have some object which implements IExample I would like for TS to allow me either having just prop c or both a, b and c and throw compilation error if I have just a and c or b and c.

Better example is using this in React component like in this SO question or even better I want to achieve same functionality like in: this ReactPropType library.

I have tried to do this via discriminating unions, but I cannot get it to work and it gets too complex when I have couple of props like these (ie requriedIf), but maybe I am just doing it all wrong. Here is example of my code:

export interface INetworkRequestStatusMessageDefaultProps {
  testID?: string;
  networkRequestStatus: NetworkRequestStatus;
}
export interface INetworkRequestStatusMessageLoadingProps {
  loadingMessageTranslationKey: string;
  firstTimeUserMessageTranslationKey: string;
  firstTimeUserCallOnActionTranslationKey: string;
  onPressFirstTimeUser: () => void;
  dataLoaded: boolean;
  errorMessageTranslationKey: never;
  errorCallOnActionTranslationKey: never;
  networkErrorMessageTranslationKey: never;
  onPressError: () => never;
  deviceConnectedToNetwork: never;
}
export interface INetworkRequestStatusMessageFisrtTimeUserProps {
  firstTimeUserMessageTranslationKey: string;
  firstTimeUserCallOnActionTranslationKey: string;
  onPressFirstTimeUser: () => void;
  dataLoaded: boolean;
  loadingMessageTranslationKey: never;  
  errorMessageTranslationKey: never;
  errorCallOnActionTranslationKey: never;
  networkErrorMessageTranslationKey: never;
  onPressError: () => never;
  deviceConnectedToNetwork: never;
}
export interface INetworkRequestStatusMessageErrorProps {
  errorMessageTranslationKey: string;
  errorCallOnActionTranslationKey: string;
  networkErrorMessageTranslationKey: string;
  onPressError: () => void;
  deviceConnectedToNetwork: boolean;
  firstTimeUserMessageTranslationKey: undefined;
  firstTimeUserCallOnActionTranslationKey: never;
  onPressFirstTimeUser: () => never;
  dataLoaded: never;
  loadingMessageTranslationKey: never;
}

export type NetworkRequestStatusMessageProps =
  | (INetworkRequestStatusMessageDefaultProps &
      (
        | INetworkRequestStatusMessageLoadingProps
        | INetworkRequestStatusMessageFisrtTimeUserProps
        | INetworkRequestStatusMessageErrorProps))
  | (INetworkRequestStatusMessageDefaultProps &
      INetworkRequestStatusMessageErrorProps &
      INetworkRequestStatusMessageFisrtTimeUserProps &
      INetworkRequestStatusMessageLoadingProps);

And here is how I am using it:

 <NetworkRequestStatusMessage
            networkRequestStatus={this.props.networkRequestStatus}
            deviceConnectedToNetwork={this.props.deviceConnectedToNetwork}
            dataLoaded={this.props.savedWorkoutsLoaded}
            errorCallOnActionTranslationKey={"savedWorkouts.retry"}
            errorMessageTranslationKey={"savedWorkouts.error"}
            firstTimeUserCallOnActionTranslationKey={"savedWorkouts.callOnActionGoToCW"}
            firstTimeUserMessageTranslationKey={"savedWorkouts.firstTimeUser"}
            loadingMessageTranslationKey={"savedWorkouts.loading"}
            networkErrorMessageTranslationKey={"savedWorkouts.networkError"}
            onPressError={this.props.load}
            onPressFirstTimeUser={() => {
              tron.log("Clicked on go to CW");
            }}
            testID={"SAVED_WORKOUTS"}
          />

But TS is yelling at me saying that:

types of property 'firstTimeUserMessageTranslationKey' are incompatible.
        Type 'string' is not assignable to type 'undefined'

And the whole idea behind my code was to make sure that if developer using NetworkRequestStatusMessage must specify either all props or for example all of these props (or none of them): errorMessageTranslationKey, errorCallOnActionTranslationKey, networkErrorMessageTranslationKey, onPressError and similarly for INetworkRequestStatusMessageFisrtTimeUserProps

hyperN
  • 2,674
  • 9
  • 54
  • 92
  • I've took a try in answering this, but your question doesn't look like [mcve] – Nurbol Alpysbayev Feb 08 '19 at 06:44
  • Thanks for your answer below, but could you please tell me what is exactly troubling you with the question? Missing code or that it is too tightly coupled with my concrete implementation or smth else ? – hyperN Feb 08 '19 at 06:52
  • 1
    It's too many details, hence too much cognitive load for us, you should simplify to the very basic problem. – Nurbol Alpysbayev Feb 08 '19 at 06:53
  • Thanks for the feedback, I am on my way to the office now, I'll trie to simplify it a bit later :) – hyperN Feb 08 '19 at 06:55

1 Answers1

4

You should use a union for that:

type Example = {
  a: string;
  b: () => void;
  c: boolean;
} | {
  c: boolean;
}
Nurbol Alpysbayev
  • 19,522
  • 3
  • 54
  • 89
  • Thanks for your answer, could you please take look at my code, I have tried using union to solve my problem, but I am getting error for incompatible keys (and I have had put the same keys but with `never` type because of this issue: [link](https://github.com/Microsoft/TypeScript/issues/12815#issuecomment-266250230) – hyperN Feb 08 '19 at 06:50
  • Please accept my apologies for not replying sooner, but after looking at my code I have messed up smth. with discriminated unions. This is how it is done, Using this & TypeGuards saved my day. Thanks again! – hyperN Feb 20 '19 at 07:07
  • Is there a way to achieve that by using conditional types https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html ? Your solution works, but it introduces duplicate code. – Sir hennihau Jun 23 '20 at 09:54