Typescript – handling conditional parameters with

Issue

I am currently writing a wrapper component around Material UI’s <TreeView. – within this wrapper, I want to mimic some of the functionality to enable me to pass in props to the treeview. Specifically, the multiSelect prop.

Now, the MultiSelect prop for the TreeView works like this. If its a multi select, it take a string[] as a parameter. The handleSelect fires an event with an array of selected strings. If the multiSelect={false} – the inverse is true. Single string parameter for selected, and single string parameter for the raised event on handleSelect. The Typescript interface for Material UI TreeView illustrates this nicely: I’ve lifted the below straight out of the source code.

export interface MultiSelectTreeViewProps extends TreeViewPropsBase {
  /**
   * Selected node ids. (Uncontrolled)
   * When `multiSelect` is true this takes an array of strings; when false (default) a string.
   * @default []
   */
  defaultSelected?: string[];
  /**
   * Selected node ids. (Controlled)
   * When `multiSelect` is true this takes an array of strings; when false (default) a string.
   */
  selected?: string[];
  /**
   * If true `ctrl` and `shift` will trigger multiselect.
   * @default false
   */
  multiSelect?: true;
  /**
   * Callback fired when tree items are selected/unselected.
   *
   * @param {React.SyntheticEvent} event The event source of the callback
   * @param {string[] | string} nodeIds Ids of the selected nodes. When `multiSelect` is true
   * this is an array of strings; when false (default) a string.
   */
  onNodeSelect?: (event: React.SyntheticEvent, nodeIds: string[]) => void;
}

export interface SingleSelectTreeViewProps extends TreeViewPropsBase {
  /**
   * Selected node ids. (Uncontrolled)
   * When `multiSelect` is true this takes an array of strings; when false (default) a string.
   * @default []
   */
  defaultSelected?: string;
  /**
   * Selected node ids. (Controlled)
   * When `multiSelect` is true this takes an array of strings; when false (default) a string.
   */
  selected?: string;
  /**
   * If true `ctrl` and `shift` will trigger multiselect.
   * @default false
   */
  multiSelect?: false;
  /**
   * Callback fired when tree items are selected/unselected.
   *
   * @param {React.SyntheticEvent} event The event source of the callback
   * @param {string[] | string} nodeIds Ids of the selected nodes. When `multiSelect` is true
   * this is an array of strings; when false (default) a string.
   */
  onNodeSelect?: (event: React.SyntheticEvent, nodeIds: string) => void;
}

Now, my question – within my component that wraps it (ToolTreeView), I’ve got this defined, where the selected, and multi props are coming in from my component before being passed down. See below:

 <TreeView
        multiSelect={multi}
        defaultCollapseIcon={<ExpandMoreIcon />}
        defaultExpandIcon={<ChevronRightIcon />}
        expanded={expanded}
        selected={selected}
        onNodeToggle={handleToggle}
        onNodeSelect={!multi ? handleSingleNodeSelect : handleNodeSelect}
        sx={{
          height: 390,
          flexGrow: 1,
          overflowY: "auto",
        }}
      >

Here’s how I’ve handled that. Somewhat mimicking the other source code:

interface Base {
  data: RawNodeDatum | RawNodeDatum[];
  expand?: boolean;
  fieldName: string;
}

interface SingleToolTreeViewProps extends Base {
  handleSelect?: (event: React.SyntheticEvent, nodeIds: string) => void;
  selected: any;
  setSelected?: (string) => void;
  multi: false;
}

interface MultiToolTreeViewProps extends Base {
  handleSelect?: (event: React.SyntheticEvent, nodeIds: string[]) => void;
  selected: any;
  setSelected?: (string) => void;
  multi: true;
}

const ToolTreeView = (
  props: SingleToolTreeViewProps | MultiToolTreeViewProps
) => {
...etc

Of note here, is my selected: any in both interfaces. This is the only thing that seemed to work without Typescript throwing an error. I thought I’d be able to write:

selected: string;

and

selected: string[];

But these get combined down into string | string[] when being passed to the TreeView, and Typescript complains with

(event: any, nodeIds: any) => void; onNodeSelect: (event: any, nodeIds: any) => void; sx: { ...; }; }' is not assignable to type 'MultiSelectTreeViewProps'.
    Types of property 'selected' are incompatible.
      Type 'string | string[]' is not assignable to type 'string[]'.
        Type 'string' is not assignable to type 'string[]'.ts(2322)

How can I maintain my types and get rid of the error? any works for now, but it’s a hack. I’d like to know more about how this sort of conditional parameter should be setup in Typescript. Should I be casting to string and string[] respectively or something?

Solution

I skimmed through the details of your post, would this suffice?

type Props<T extends string | string[], M = T extends any[] ? true : false> = {
  data: Date | Date[];
  expand?: boolean;
  fieldName: string;
  handleSelect?: (event: Event, nodeIds: T) => void;
  selected: T;
  setSelected?: (a: string) => void;
  multi: M;
}

const ToolTreeView = <T extends string | string[]>(
  props: Props<T>
) => props;

const a = ToolTreeView({
    multi: true,
    data: new Date(),
    fieldName: 'foo',
    selected: ['a', 'b'],
});

const b = ToolTreeView({
    multi: false,
    data: new Date(),
    fieldName: 'foo',
    selected: 'dsada',
});

// Should fail, since muti==false & typeof selected == array
const c = ToolTreeView({
    multi: false,
    data: new Date(),
    selected: 'dsada',
    fieldName: ['a', 'b'],
});

playground

Answered By – Olian04

This Answer collected from stackoverflow, is licensed under cc by-sa 2.5 , cc by-sa 3.0 and cc by-sa 4.0

Leave a Reply

(*) Required, Your email will not be published