react-dnd/index.d.ts file in this repo and had problems.[x] I want to talk about react-dnd/index.d.ts.
[ ] strictNullChecks doesn't typecheck
Sorry for the format of this as a "grab-bag", but this is to help me collect my thoughts. I am willing to take stabs at PRs for these things but first I want to make sure that I'm not doing something wrong, and then agree on a design so that I know my PR will be accepted.
strictNullChecks doesn't typecheck
import * as DND from "react-dnd";
import * as React from "react";
interface PDraggable {
connectDrag: DND.DragElementWrapper<{}>;
isDragging: boolean;
}
let draggableSpec: DND.DragSourceSpec<PDraggable> = {
beginDrag: (props: PDraggable) => ({}),
};
let draggableCollector = (connect: DND.DragSourceConnector, monitor: DND.DragSourceMonitor) => {
return {
connectDrag: connect.dragSource(),
isDragging: monitor.isDragging()
}
};
@DND.DragSource("my-draggable-direct", draggableSpec, draggableCollector)
export class MyDraggable extends React.Component<PDraggable,{}> {
constructor(props: PDraggable) {
super(props);
}
render() {
const { connectDrag, isDragging } = this.props;
return connectDrag(<span className="node-source">{'\u2683'}</span>);
}
}
Actual:
Unable to resolve signature of class decorator when called as an expression.
Type 'DndComponentClass<PDraggable>' is not assignable to type 'typeof MyDraggable'.
Type 'DndComponent<PDraggable, any>' is not assignable to type 'MyDraggable'.
Types of property 'render' are incompatible.
Type '() => Element | null' is not assignable to type '() => ReactElement<any>'.
Type 'Element | null' is not assignable to type 'ReactElement<any>'.
Type 'null' is not assignable to type 'ReactElement<any>'.
Expected: this should typecheck. Why does DndComponent change render to Element | null? I can't find where that's happening in the declaration file. Take off strictNullChecks and this typechecks and works at runtime.
Cannot decorate a subclass of React.Component
import * as DND from "react-dnd";
import * as React from "react";
interface PDraggable {
connectDrag: DND.DragElementWrapper<{}>;
isDragging: boolean;
}
interface SDraggable {
}
class ComponentSubclass<P, S> extends React.Component<P, S> {
myProp = 42;
}
let draggableSpec: DND.DragSourceSpec<PDraggable> = {
beginDrag: (props: PDraggable) => ({}),
};
let draggableCollector = (connect: DND.DragSourceConnector, monitor: DND.DragSourceMonitor) => {
return {
connectDrag: connect.dragSource(),
isDragging: monitor.isDragging()
}
};
@DND.DragSource("my-draggable", draggableSpec, draggableCollector)
export class MyDraggable extends ComponentSubclass<PDraggable, SDraggable> {
constructor(props: PDraggable) {
super(props);
}
render() {
const { connectDrag, isDragging } = this.props;
return connectDrag(<span className="node-source">{'\u2683'}</span>);
}
}
Actual:
Unable to resolve signature of class decorator when called as an expression.
Type 'DndComponentClass<PDraggable>' is not assignable to type 'typeof MyDraggable'.
Type 'DndComponent<PDraggable, any>' is not assignable to type 'MyDraggable'.
Property 'myProp' is missing in type 'DndComponent<PDraggable, any>'.
Expected: The decoration returns a new subclass which has myProp as well DndComponent. Pretty sure this is doable.
Cannot add properties directly to a decorated Component
import * as DND from "react-dnd";
import * as React from "react";
interface PDraggable {
connectDrag: DND.DragElementWrapper<{}>;
isDragging: boolean;
}
interface SDraggable {
}
class ComponentSubclass<P, S> extends React.Component<P, S> {
myProp = 42;
}
let draggableSpec: DND.DragSourceSpec<PDraggable> = {
beginDrag: (props: PDraggable) => ({}),
};
let draggableCollector = (connect: DND.DragSourceConnector, monitor: DND.DragSourceMonitor) => {
return {
connectDrag: connect.dragSource(),
isDragging: monitor.isDragging()
}
};
@DND.DragSource("my-draggable-direct", draggableSpec, draggableCollector)
export class MyDraggableDirect extends React.Component<PDraggable, SDraggable> {
myProp = 42;
constructor(props: PDraggable) {
super(props);
}
render() {
const { connectDrag, isDragging } = this.props;
return connectDrag(<span className="node-source">{'\u2683'}</span>);
}
}
Same error as above. Not only can I not subclass, but my draggable can't have any additional members directly. This is limiting.
Non-optional properties injected by collecting functions are mandatory in literal JSX
import * as DND from "react-dnd";
import * as React from "react";
interface PDraggable {
connectDrag: DND.DragElementWrapper<{}>;
isDragging: boolean;
}
interface SDraggable {
}
class ComponentSubclass<P, S> extends React.Component<P, S> {
myProp = 42;
}
let draggableSpec: DND.DragSourceSpec<PDraggable> = {
beginDrag: (props: PDraggable) => ({}),
};
let draggableCollector = (connect: DND.DragSourceConnector, monitor: DND.DragSourceMonitor) => {
return {
connectDrag: connect.dragSource(),
isDragging: monitor.isDragging()
}
};
@DND.DragSource("my-draggable-direct", draggableSpec, draggableCollector)
export class MyDraggable extends React.Component<PDraggable, SDraggable> {
constructor(props: PDraggable) {
super(props);
}
render() {
const { connectDrag, isDragging } = this.props;
return connectDrag(<span className="node-source">{'\u2683'}</span>);
}
}
export class MyWidget extends React.Component<{}, {}> {
constructor(props: {}) {
super(props);
}
render() {
return <MyDraggable />
}
}
Actual:
Error:
[ts] Property 'connectDrag' is missing in type 'IntrinsicAttributes & IntrinsicClassAttributes<MyDraggable> & { children?: ReactNode; } & PDragga...'.
[ts] Property 'isDragging' is missing in type 'IntrinsicAttributes & IntrinsicClassAttributes<MyDraggable> & { children?: ReactNode; } & PDragga...'.
Expected:
These props are injected by the collector function. I don't need to pass them here. All my options for fixing this are slightly annoying. I could declare collector-injected-props as optional, but, once strictNullChecks is fixed (above), I'll have to guard against undefined whenever I want to use the props. This is true even though, AFAIK, they'll always be defined (though I don't know how that magic works. Does DndComponent hook componentWillReceiveProps?) Alternatively, I can pass collectProp={null as any} where ever I instantiate my draggable component, knowing that dnd magic will put the real values into props before my draggable component ever renders.
I don't know if it's possible to fix this using TypeScript 2.1 or later. Basically, MyDraggableComponent is instantiated with vanilla Props, but then some magic happens and the thing that goes to the render function and spec callbacks is Props & MagicCollectorStuff. I don't know how to express this magic.
I think I've just run into the same problem regarding your 'Cannot add properties directly to a decorated Component' item. It is incredibly limiting being unable to decorate a class with any non-React.Component properties, and unfortunately appears to make the current typings completely unusable for me...
It's possible to work around those issues, but you have to break out of the type system.
Make your class as normal, but don't use decorators.
interface PMyClass {}
class _MyClass extends React.Component<PMyClass, any> {
life = 42;
}
let MyClass = DND.DragDropContext(TouchBackend({ enableMouseEvents: true }))(_MyClass) as any;
as any is necessary here because of another strange issue, which is that you see this if you don't use it:
[ts] JSX element attributes type '({ children?: ReactNode; } & PMyClass) | ({ children?: ReactNode; } & PMyClass)' may not be a union type
You still get types for the monitors and things, but not your decorated components. When you instantiate them in JSX they won't be type-checked for prop completeness any more. This is a blessing and a curse. The blessing is that #4 issue above goes away.
I've given it a fair amount of effort to better type the decorator functions DragSource, DropTarget, DragDropContext, DragLayer, but I haven't been able to come up with anything. Right now I'm blocked by an issue with generics that is either a bug or a fundamental mistake in my mental model of TS's type system. I am waiting to hear back on this issue: https://github.com/Microsoft/TypeScript/issues/13149
I came across this same issue myself recently but opted for a slightly different solution:
import React from "react";
import { DragDropContext } from "react-dnd";
import HTML5Backend from "react-dnd-html5-backend";
export const DragDropContextProvider = DragDropContext(HTML5Backend)(
(props: any) => React.Children.only(props.children)
);
fairly reusable. just wrap it around the components that participate in DnD:
import { DragDropContextProvider } from "dndHelpers";
<DragDropContextProvider>
<MyApp>
<OneOfMyAppComponent/>
<AnotherOneOfMyAppComponents/>
</MyApp>
</DragDropContextProvider>
@masonk THANK YOU!! I couldn麓t find how to get rid off the ts] JSX element attributes type '({ children?: ReactNode; } & PMyClass) | ({ children?: ReactNode; } & PMyClass)' may not be a union type and your as any helped me so thank you very much!
Hi, I find the typings to work nicely when using a simple class - but as soon as the class has members - or not the weird return type on render() - it breaks. It would be greatly appreciated if someone would find a typing recipe that works better.
However, to get around this, I use a simple wrapper class that take the actual content as react children. That wrapper has properties to deal with the events from React-dnd. This way the client / caller can orchestrate the event flow between the drop-zone and the content waiting for items.
Example, for native file uploads, excerpt:
interface IPublicInterface {
uploadFiles: (files: File[]) => void
}
interface IDropAreaProps extends IPublicInterface {
connectDropTarget: ConnectDropTarget
isOver: boolean
canDrop: boolean
}
const cardTarget: DropTargetSpec<IDropAreaProps> = {
drop(props, monitor) {
// console.log()
props.uploadFiles((monitor.getItem() as any).files)
},
}
// This component adds drag & drop target to project main (or other!) for native files
// Note: This could also support other kinds of internal or external data in the future.
// Note: This component is needed because the typings for decorators is not tasty and nice, see more here: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/13404
@DropTarget(NativeTypes.FILE, cardTarget, (connect, monitor: DropTargetMonitor) => ({
connectDropTarget: connect.dropTarget(),
isOver: monitor.isOver(),
canDrop: monitor.canDrop()
}))
export class ProjectMainDropAreaRaw extends React.Component<IDropAreaProps, any> {
render(): JSX.Element | false {
return this.props.connectDropTarget(
<div className={`project-main drop-target ${this.props.isOver ? "hoovered" : ""} ${(this.props.canDrop ? "can" : "cannot") + "-drop"}`}>
{this.props.children}
</div>
)
}
}
export const ProjectMainDropArea = ProjectMainDropAreaRaw as React.ComponentClass<IPublicInterface>
Most helpful comment
I came across this same issue myself recently but opted for a slightly different solution:
fairly reusable. just wrap it around the components that participate in DnD: