dnd-kit icon indicating copy to clipboard operation
dnd-kit copied to clipboard

Typesafe `data` - TypeScript

Open RemyMachado opened this issue 2 years ago • 4 comments

The data property is great for handling logic. However, it doesn't seem to be typesafe.

It would be nice if the DndContext & the hooks could be generic in order to keep total control of the typing.

Currently I'm using the optional chain operator everywhere ?. I need to handle this data.

RemyMachado avatar Nov 12 '22 14:11 RemyMachado

Yes! Wanted to second this and bump it up... not a typescript pro but something like this on the consumer side I think is what we're looking for

const {  } = useDraggable<DataProps>({ id,  data: { name } });

or

const {  } = useDraggable<{data: DataProps}>({ id,  data: { name } });

and some way to cast it when consuming it...

<DndContext
    onDragStart={({ active }: {active: Active<DataProps>}) => {
      //...
    }}
>

or again...

<DndContext
    onDragStart={({ active }: {active: Active<{data: DataProps}>}) => {
      //...
    }}
>

Open to people's thoughts...

paulwongx avatar Mar 26 '23 18:03 paulwongx

For my case I simply wrap the hooks and components in a stricter type, and import and use that stricter version instead

import {
  Active,
  Collision,
  DndContextProps,
  DndContext as OriginalDndContext,
  Over,
  Translate,
  UseDraggableArguments,
  UseDroppableArguments,
  useDraggable as useOriginalDraggable,
  useDroppable as useOriginalDroppable,
} from "@dnd-kit/core";
import { Element } from "slate";

interface DroppableData {
  id: string;
  position: string;
}
interface UseDroppableTypesafeArguments extends Omit<UseDroppableArguments, "data"> {
  data: DroppableData;
}
export function useDroppable(props: UseDroppableTypesafeArguments) {
  return useOriginalDroppable(props);
}

interface DraggableData {
  element: Element;
}
interface UseDraggableTypesafeArguments extends Omit<UseDraggableArguments, "data"> {
  data: DraggableData;
}
export function useDraggable(props: UseDraggableTypesafeArguments) {
  return useOriginalDraggable(props);
}

interface TypesafeActive extends Omit<Active, "data"> {
  data: React.MutableRefObject<DraggableData | undefined>;
}
interface TypesafeOver extends Omit<Over, "data"> {
  data: React.MutableRefObject<DroppableData | undefined>;
}
interface DragEvent {
  activatorEvent: Event;
  active: TypesafeActive;
  collisions: Collision[] | null;
  delta: Translate;
  over: TypesafeOver | null;
}
export interface DragStartEvent extends Pick<DragEvent, "active"> {}
export interface DragMoveEvent extends DragEvent {}
export interface DragOverEvent extends DragMoveEvent {}
export interface DragEndEvent extends DragEvent {}
export interface DragCancelEvent extends DragEndEvent {}
export interface DndContextTypesafeProps
  extends Omit<
    DndContextProps,
    "onDragStart" | "onDragMove" | "onDragOver" | "onDragEnd" | "onDragCancel"
  > {
  onDragStart?(event: DragStartEvent): void;
  onDragMove?(event: DragMoveEvent): void;
  onDragOver?(event: DragOverEvent): void;
  onDragEnd?(event: DragEndEvent): void;
  onDragCancel?(event: DragCancelEvent): void;
}
export function DndContext(props: DndContextTypesafeProps) {
  return <OriginalDndContext {...props} />;
}

joulev avatar May 27 '23 06:05 joulev

Thanks @joulev

We want the hooks to return typesafe active & over, so I've modified your example to accommodate:

// useDroppable()
type UseDroppableTypesafeReturnValue = Omit<
  ReturnType<typeof useOriginalDroppable>,
  'active' | 'over'
> & {
  active: TypesafeActive | null;
  over: TypesafeOver | null;
};

export function useDroppable(props: UseDroppableTypesafeArguments) {
  return useOriginalDroppable(props) as UseDroppableTypesafeReturnValue;
}

// useDraggable()
type UseDraggableTypesafeReturnValue = Omit<
  ReturnType<typeof useOriginalDraggable>,
  'active' | 'over'
> & {
  active: TypesafeActive | null;
  over: TypesafeOver | null;
};

export function useDraggable(props: UseDraggableTypesafeArguments) {
  return useOriginalDraggable(props) as UseDraggableTypesafeReturnValue;
}

psychedelicious avatar Jun 30 '23 14:06 psychedelicious

another convenient way to patch types:

import {
  Active,
  CancelDrop,
  Collision,
  CollisionDetection,
  DndContext as OriginalDndContext,
  DndContextProps,
  DragCancelEvent,
  DragEndEvent,
  DragMoveEvent,
  DragOverEvent,
  DragStartEvent,
  DroppableContainer,
  Over,
  useDndMonitor as baseUseDndMonitor,
  useDraggable as baseUseDraggable,
  UseDraggableArguments,
  useDroppable as baseUseDroppable,
  UseDroppableArguments,
} from "@dnd-kit/core";

export const typedDnd = <DragData, DropData>() => {
  type TypesafeActive = Omit<Active, "data"> & {
    data: React.MutableRefObject<DragData | undefined>;
  };
  type TypesafeOver = Omit<Over, "data"> & {
    data: React.MutableRefObject<DropData | undefined>;
  };

  type ContextProps = Omit<
    DndContextProps,
    | "onDragStart"
    | "onDragMove"
    | "onDragOver"
    | "onDragEnd"
    | "onDragCancel"
    | "cancelDrop"
    | "collisionDetection"
  > & {
    onDragStart?: (
      e: Omit<DragStartEvent, "active"> & {
        active: TypesafeActive;
      },
    ) => void;
    onDragMove?: (
      e: Omit<DragMoveEvent, "active" | "over"> & {
        active: TypesafeActive;
        over: TypesafeOver | null;
      },
    ) => void;
    onDragOver?: (
      e: Omit<DragOverEvent, "active" | "over"> & {
        active: TypesafeActive;
        over: TypesafeOver | null;
      },
    ) => void;
    onDragEnd?: (
      e: Omit<DragEndEvent, "active" | "over"> & {
        active: TypesafeActive;
        over: TypesafeOver | null;
      },
    ) => void;
    onDragCancel?: (
      e: Omit<DragCancelEvent, "active" | "over"> & {
        active: TypesafeActive;
        over: TypesafeOver | null;
      },
    ) => void;
    cancelDrop?: (
      e: Omit<Parameters<CancelDrop>[0], "active" | "over"> & {
        active: TypesafeActive;
        over: TypesafeOver | null;
      },
    ) => ReturnType<CancelDrop>;
    collisionDetection?: (
      e: Omit<
        Parameters<CollisionDetection>[0],
        "active" | "droppableContainers"
      > & {
        active: TypesafeActive;
        droppableContainers: Array<
          Omit<DroppableContainer, "data"> & TypesafeOver
        >;
      },
    ) => Array<Omit<Collision, "data"> & TypesafeOver>;
  };

  const DndContext: React.NamedExoticComponent<ContextProps> =
    OriginalDndContext as any;

  const useDndMonitor: (
    args: Pick<
      ContextProps,
      "onDragStart" | "onDragMove" | "onDragOver" | "onDragEnd" | "onDragCancel"
    >,
  ) => void = baseUseDndMonitor as any;

  const useDraggable: (
    args: Omit<UseDraggableArguments, "data"> & { data: DragData },
  ) => Omit<ReturnType<typeof baseUseDraggable>, "active" | "over"> & {
    active: TypesafeActive | null;
    over: TypesafeOver | null;
  } = baseUseDraggable as any;

  const useDroppable: (
    args: Omit<UseDroppableArguments, "data"> & { data: DropData },
  ) => Omit<ReturnType<typeof baseUseDroppable>, "active" | "over"> & {
    active: TypesafeActive | null;
    over: TypesafeOver | null;
  } = baseUseDroppable as any;

  return { DndContext, useDndMonitor, useDraggable, useDroppable };
};

then, you can specify a contract and use the hooks and context like this:

import { typedDnd } from '@/lib/typedDnd'

type DragEvent = { card: string };
type DropEvent = { slot: number };

// fully typed!
const { DndContext, useDraggable, useDroppable, useDndMonitor } = typedDnd<
  DragEvent,
  DropEvent
>();

xandjiji avatar Jan 20 '24 21:01 xandjiji