Search code examples
reactjstypescriptgenericsnext.jsreact-tsx

Is there any way to share type between object properties only by provided value?


I want to make React table custom component with row selection in single, multiple and none selection mode. And since is refactoring process of current oversized table component I want to make it as simple as possible in case of setup and props.

I tried some attempts with infer and unions like this:

type SimpleTableSelectionOptionsSingle = {
  selected: string | null;
  onChange: (selected: string | null) => void;
};


type SimpleTableSelectionOptionsMultiple = {
  selected: string[];
  onChange: (selected: string[]) => void;
};

type SimpleTableSelectionOptions<T extends string | string[] | null> = T extends string
  ? SimpleTableSelectionOptionsSingle
  : T extends string[]
  ? SimpleTableSelectionOptionsMultiple
  : never;

type Props = {
//...other props
selectionOptions?: SimpleTableSelectionOptions<???>
}

But I'm ended up providing some default generics types to SimpleTableSelectionOptions. I wanted to specify type between string | string[] | null by provided data. And kinda obvious that null and string should be specified in pair, just like string[] and [] - empty string array (but here's no type required). I'm asking here, because I don't even know if it's possible to achieve props like:

<SimpleTable 
//...other props
selectionOptions={{
selected: 'some-id', // Since this is string
onChange: (selected) => { /* something... */ }, // selected here also needs to be string 
}}
/>

AND in the same time:

<SimpleTable 
//...other props
selectionOptions={{
selected: ['some-id', 'some-other-id'], // Since this is Array<string>
onChange: (selected) => { /* something... */ }, // selected here also needs to be Array<string> 
}}
/>

I tried also some helper functions but they're not so handy, like normal infered types and I wanted to avoid them. I know how to handle checks and setup in javascript to make it working. Only thing that I can't handle is types definitions.


Solution

  • Ok, so there we go! This is handled for selections. I needed to cast them anyway.

    const handleSelection = useCallback(
        (row: T) => {
          if (!selectionOptions) {
            return;
          }
    
          const { selected, setSelected } = selectionOptions;
    
          if (Array.isArray(selected)) {
            const typedSelected = selected as string[];
            const typedSetSelected = setSelected as (selected: string[]) => void;
    
            if (typedSelected.includes(row.id)) {
              typedSetSelected(typedSelected.filter((id) => id !== row.id));
            } else {
              typedSetSelected([...typedSelected, row.id]);
            }
          } else if (typeof selected === 'string') {
            const typedSelected = selected as string;
            const typedSetSelected = setSelected as (selected: string | null) => void;
    
            if (typedSelected === row.id) {
              typedSetSelected(null);
            } else {
              typedSetSelected(row.id);
            }
          }
        },
        [selectionOptions]
      );
    

    And here's typings:

    interface SimpleTablePropsSelectionSingle<T extends SimpleTableDataType>
      extends SimpleTablePropsBase<T> {
      selectionOptions?: {
        selected: string | null;
        setSelected: (selected: string | null) => void;
      };
    }
    
    interface SimpleTablePropsSelectionMultiple<T extends SimpleTableDataType>
      extends SimpleTablePropsBase<T> {
      selectionOptions?: {
        selected: string[];
        setSelected: (selected: string[]) => void;
      };
    }
    
    export type SimpleTableProps<T extends SimpleTableDataType> =
      | SimpleTablePropsSelectionSingle<T>
      | SimpleTablePropsSelectionMultiple<T>;
    

    With this, when selected is string[]: enter image description here