Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Typesafe data - TypeScript #935

Open
RemyMachado opened this issue Nov 12, 2022 · 4 comments · May be fixed by #1324
Open

Typesafe data - TypeScript #935

RemyMachado opened this issue Nov 12, 2022 · 4 comments · May be fixed by #1324

Comments

@RemyMachado
Copy link

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.

@paulwongx
Copy link

paulwongx commented Mar 26, 2023

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...

@joulev
Copy link

joulev commented May 27, 2023

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} />;
}

@psychedelicious
Copy link

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;
}

@JaninaWibker JaninaWibker linked a pull request Jan 18, 2024 that will close this issue
@xandjiji
Copy link

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
>();

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants