Joel Hoelting
Joel Hoelting

Reputation: 1992

Typescript: function parameter that is one of two interfaces

I was looking at this question, which I thought was related to my issue. However, it is a bit different from my use case.

I have a function called parseScanResults takes an argument that is an object. The object can be one of two types. However, typescript is throwing an error with the below code:

const ScanForm: React.FC<IScanFormProps> = ({ children, onSubmit, parseScanResults }) => {
  const [scannerActive, toggleScannerActive] = useState(false);

  const closeScanner = (): void => {
    toggleScannerActive(false);
  };

  const handleScanResults = (results: IVoucherScanResults | IBlinkCardScanResults): void => {
    const { cardString, stringMonth, stringYear } = parseScanResults(results);

    setValue('cardNumber', cardString);
    setValue('expMonth', stringMonth);
    setValue('expYear', stringYear);

    toggleScannerActive(false);
  };

  return (
    <Form onSubmit={handleSubmit(onSubmit)}>
      {children({ scannerActive, closeScanner, handleScanResults })}
    </Form>
  );
};

import CreditCardBarcodeScanner from 'src/components/scanners/credit_card_barcode_scanner';

import { IVoucherScanResults, IScannerProps, IParsedScanResults } from '../scanners/card_scanners';
import ScanForm from './scan-form';

function CreditCardBarcodeForm(): JSX.Element {
  const onSubmit = (data: { expMonth: string; expYear: string; securityCode: string; cardNumber: string }): void => {
    // Do something with form data
    console.log(data);
  };

  const parseScanResults = (results: IVoucherScanResults): IParsedScanResults => {
    const { text } = results;

    const [cardString, expirationString] = text.slice().split('/');
    const stringMonth = expirationString.slice(0, 2);
    const stringYear = expirationString.slice(2, 4);

    return { cardString, stringMonth, stringYear };
  };

  return (
    <ScanForm onSubmit={onSubmit} parseScanResults={parseScanResults}>
      {({ scannerActive, closeScanner, handleScanResults }: IScannerProps) => (
        <CreditCardBarcodeScanner
          scannerActive={scannerActive}
          closeScanner={closeScanner}
          handleScanResults={handleScanResults}
        />
      )}
    </ScanForm>
  );
}

export default CreditCardBarcodeForm;

export interface IBlinkCardScanResults {
  cardNumber: string;
  cvv: string;
  expiryDate: {
    day?: number;
    empty?: boolean;
    month: number;
    originalString?: string;
    successfullyParsed?: boolean;
    year: number;
  };
}

export interface IVoucherScanResults {
  text: string;
  timestamp: number;
  format: number;
  numBits: number;
}

export interface IParsedScanResults {
  cardString: string;
  stringMonth: string;
  stringYear: string;
}

export interface IScannerProps {
  scannerActive: boolean;
  closeScanner: () => void;
  handleScanResults: (results: IVoucherScanResults | IBlinkCardScanResults) => void;
}

export interface IScanFormProps {
  children: (props: ICardScannerProps) => React.ReactElement;
  onSubmit: (data: { expMonth: string; expYear: string; securityCode: string; cardNumber: string }) => void;
  parseScanResults: (results: IBlinkCardScanResults | IVoucherScanResults) => IParsedScanResults;
}

enter image description here

The error states:

Type '(results: IVoucherScanResults) => IParsedScanResults' is not assignable to type '(results: IBlinkCardScanResults | IVoucherScanResults) => IParsedScanResults'.
  Types of parameters 'results' and 'results' are incompatible.
    Type 'IBlinkCardScanResults | IVoucherScanResults' is not assignable to type 'IVoucherScanResults'.
      Type 'IBlinkCardScanResults' is missing the following properties from type 'IVoucherScanResults': text, timestamp, format, numBitsts(2322)

Upvotes: 2

Views: 2070

Answers (1)

Liad
Liad

Reputation: 844

Your problem is that parseScanUtils is a either a function that gets an IVoucherScanResults as a parameter, or a function that gets an IBlinkCardScanResults as a parameter, while only one is true. in this case it looks like your component is receiving the first of the two.

the main point is that there is a difference between having a union of functions where each one gets a specific parameter type and having one function whose parameter is a union of two types.

  parseScanResults:
    ((results: IBlinkCardScanResults) => IParsedScanResults)
    | ((results: IVoucherScanResults) => IParsedScanResults);

vs.

parseScanResults:
    ((results: IBlinkCardScanResults | IVoucherScanResults) => IParsedScanResults)

EDIT

what you can do is use a generic and instead of typing your component function you can explicitly type your parameter:

let's first make the interface generic:

export interface IScanFormProps<T extends IBlinkCardScanResults | IVoucherScanResults> {
  children: (props: ICardScannerProps) => React.ReactElement;
  onSubmit: (data: { expMonth: string; expYear: string; securityCode: string; cardNumber: string }) => void;
  parseScanResults: (results: T) => IParsedScanResults;
}

than you can update your functional component like this:

const ScanForm = <T extends IBlinkCardScanResults | IVoucherScanResults>({ children, onSubmit, parseScanResults }: T) => {

and your handleScanResults function:

const handleScanResults = (results: T): void => {
  ...rest of code...
}

then all that's left to do is call the component with the wanted type (example for IBlinkCardScanResults):

<ScanForm<IBlinkCardScanResults> onSubmit={onSubmit} parseScanResults={parseScanResults}>

I believe it should work now

Upvotes: 2

Related Questions