dnd-kit
dnd-kit copied to clipboard
Typesafe `data` - TypeScript
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.
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...
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} />;
}
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;
}
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
>();