Search code examples
reactjstypescripttypesreact-dnd

React DnD / TypeScript: How to use getDropResult?


In my React web app, I have a number of react-dnd drop targets:

const DropTarget = () => {
  const [{ canDrop, isOver }, drop] = useDrop(() => ({
    accept: 'MyDnDItemType',
    drop: () => ({ name: 'name of this drop target' })
  }))
   ...
}  

and items to drag around and drop in those targets:

const DraggableItem = () => {
  const [{ isDragging }, drag] = useDrag({
    type: 'MyDnDItemType'
  })
   ...

When a DraggableItem is dropped into one of the DropTargets, I would like to get the drop target name. The react-dnd useDrag docs say in the end(item, monitor) section:

If [...] the drop target specified a drop result by returning a plain object from its drop() method, it will be available as monitor.getDropResult().

However, when I add an end(...) function in useDrag(...) and attempt to access the plain object returned, I get a TypeScript compiler error Object is of type 'unknown'.ts(2571).

const DraggableItem = () => {
  const [{ isDragging }, drag] = useDrag({
    end: (item, monitor) => {
      const dropResult = monitor.getDropResult();
      // console.log('dropResult:', dropResult)
      // sample log output:
      //   dropResult: {dropEffect: 'move', name: 'name of this drop target'}

      // PROBLEM: this fails to compile with
      //   Object is of type 'unknown'.ts(2571)
      console.log('name:', dropResult.name)
    },
    type: 'MyDnDItemType'
  })
   ...

I also tried end: (item, monitor: DragSourceMonitor) ... and, after reading about this somewhat related issue and delving into the source code, tried to use an explicit type:

type MyType = {
  name: string;
};

const DraggableItem = () => {
  const [{ isDragging }, drag] = useDrag({
    end: (item, monitor: DragSourceMonitor<MyType>) => {
     ... (as above) ...

but the error persists.

How can I access the plain object returned by getDropResult() ?


Solution

  • Figured it out: Was on the right track, but didn't follow through.

    Some quick background info: Until a couple of minutes ago, I knew only very little about TypeScript and, as for generics, only what I remember about C++ templates from ~20 years ago; probably close to nothing. It's not that hard, though:

    The DragSourceMonitor interface takes two type parameters DragObject and DropResult, which per default are unknown:

    export interface DragSourceMonitor<DragObject = unknown, DropResult = unknown>
    

    One of the methods on that interface is getDropResult which takes a single type parameter T, using DropResult as default:

    getDropResult<T = DropResult>(): T | null
    

    This provides two entry points to fix the compiler error:

    Either use own types to pass to DragSourceMonitor, e.g.

    type MyDragObject = {
      name: string;
    }
    
    type MyDropResult = {
      name: string;
    }
    
    const DraggableTreeItem = (props: TreeItemProps) => {
    
      const [{ isDragging }, drag] = useDrag({
        collect: (monitor: DragSourceMonitor) => ({
          isDragging: monitor.isDragging()
        }),
       ...
      end: (item, monitor: DragSourceMonitor<MyDragObject, MyDropResult>) => {
       ...
    

    or pass the type to use only when calling getDropResult(...), e.g.

    type MyDropResult = {
      name: string;
    }
    
    const DraggableTreeItem = (props: TreeItemProps) => {
    
      const [{ isDragging }, drag] = useDrag({
       ...
      end: (item, monitor: DragSourceMonitor) => {
          const dropResult = monitor.getDropResult<MyDropResult>()
          // getDropResult returns `T | null`, so need to test
          // against null to fix `Object is possibly 'null'.ts(2531)`
          if (dropResult) {
            console.log('name:', dropResult.name)
          }
         ...
      }
       ...