KarimMokhtar / react-drag-drop-files

Light and simple Reactjs drag and drop files library to use with very flexible options to change, so you put whatever the design you want for your drop-area. Users can drag and drop or even select the file anywhere in the window.
MIT License
248 stars 91 forks source link

Narrow handle function types based on "multiple" prop #139

Open iGoodie opened 6 months ago

iGoodie commented 6 months ago

Hey! 👋🏼 Thanks a lot for implementing such a time-saver library!

When multiple prop is set to a falsy value, it is guaranteed to have exactly 1 file. However the typings on onDrop, onSelect and handleChange are widely typed to accommodate both cases. I'd expect them to be narrowed to accept File as their first argument when multiple is set to falsy value.


Current types:

<FileUploader
    onDrop={file => {}}
    // ^? (property) onDrop?: ((arg0: File | Array<File>) => void)
/>

<FileUploader
    multiple={false}
    onDrop={file => {}}
    // ^? (property) onDrop?: ((arg0: File | Array<File>) => void)
/>

<FileUploader
    multiple
    onDrop={files => {}}
    // ^? (property) onDrop?: ((arg0: File | Array<File>) => void)
/>

Expected narrowed types:

<FileUploader
    onDrop={file => {}}
    // ^? (property) onDrop?: ((file: File) => void)
/>

<FileUploader
    multiple={false}
    onDrop={file => {}}
    // ^? (property) onDrop?: ((file: File) => void)
/>

<FileUploader
    multiple
    onDrop={files => {}}
    // ^? (property) onDrop?: ((files: Array<File>) => void)
/>

This could be achieved by having Props implemented as a generic type. Theoretically with something like:

type FileFn<M extends boolean> = [boolean] extends [M]
  ? (arg: File | File[]) => void
  : M extends true
  ? (files: File[]) => void
  : (file: File) => void;

type Props<M extends boolean = false> = {
  multiple?: M;
  onDrop?: FileFn<M>;
  onSelect?: FileFn<M>;
  handleChange?: FileFn<M>;
  // ... other props are omitted
};

function FileUploader<M extends boolean = false>(props: Props<M>) {
  return <>{/* ... code is omitted. Returning props instead, to be able to test */}</>;
}

Tests:

type SingleTest = FileFn<false>;
//   ^? type SingleTest = (file: File) => void;

type MultiTest = FileFn<true>;
//   ^? type MultiTest = (files: File[]) => void

const dynamicValue = Math.random() >= 0.5;
<FileUploader multiple={dynamicValue} onDrop={} />;
//                                                               ^? (arg: File | File[]) => void

<FileUploader multiple handleChange={} />;
//                                   ^? (property) handleChange?: ((files: File[]) => void)

<FileUploader multiple={false} handleChange={} />;
//                                               ^? (property) handleChange?: ((file: File) => void)

<FileUploader handleChange={} />;
//                     ^? (property) handleChange?: ((file: File) => void)