Skip to content

Commit 6b587ca

Browse files
committed
Fix handling of partial / undefined props in React 18 TypeScript
Fixes #807
1 parent a0b6e21 commit 6b587ca

6 files changed

Lines changed: 926 additions & 1342 deletions

File tree

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ lint:
1515
@$(BIN)/eslint lib
1616
@$(BIN)/tsc --noEmit
1717
@$(BIN)/tsc -p typings
18+
@$(BIN)/tsc -p typings/tsconfig.react18.json
1819

1920
# tsup emits cjs + esm + dts into build/cjs (and rewrites build/cjs/cjs.js to the
2021
# legacy module.exports === Draggable shape). webpack emits the UMD global bundle.

lib/Draggable.tsx

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,20 @@ class Draggable extends React.Component<Partial<DraggableProps>, DraggableState>
5151

5252
static displayName?: string = 'Draggable';
5353

54-
// The index-signature annotation is load-bearing: without it tsc infers the
55-
// PropTypes.Requireable<...> types and emits `import PropTypes from 'prop-types'`
56-
// into the generated public .d.ts, forcing consumers to install
57-
// @types/prop-types (the v4.5.0 hand-written typings had no prop-types dep).
54+
// Both the annotation and the `?` are load-bearing:
55+
// - The index-signature annotation stops tsc from inferring the
56+
// PropTypes.Requireable<...> types and emitting `import PropTypes from
57+
// 'prop-types'` into the generated public .d.ts, which would force consumers
58+
// to install @types/prop-types (the v4.5.0 hand-written typings had none).
59+
// - The `?` keeps `propTypes` from being a *required* member of the public
60+
// type. React <= 18's JSX LibraryManagedAttributes only consults a
61+
// component's `propTypes` when it is required (`C extends {propTypes: ...}`);
62+
// when it does, this index-signature `propTypes` makes `defaultProps` stop
63+
// marking props optional, so consumers are forced to pass every prop.
64+
// Optional dodges that branch; React 19 ignores `propTypes` entirely. The
65+
// typings/tsconfig.react18.json check guards against a regression here.
5866
// Do not remove. See lib/DraggableCore.tsx for the same guard.
59-
static propTypes: {[key: string]: unknown} = {
67+
static propTypes?: {[key: string]: unknown} = {
6068
// Accepts all props <DraggableCore> accepts.
6169
...DraggableCore.propTypes,
6270

lib/DraggableCore.tsx

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,12 +74,20 @@ export default class DraggableCore extends React.Component<Partial<DraggableCore
7474

7575
static displayName: string | undefined = 'DraggableCore';
7676

77-
// The index-signature annotation is load-bearing: without it tsc infers the
78-
// PropTypes.Requireable<...> types and emits `import PropTypes from 'prop-types'`
79-
// into the generated public .d.ts, forcing consumers to install
80-
// @types/prop-types (the v4.5.0 hand-written typings had no prop-types dep).
77+
// Both the annotation and the `?` are load-bearing:
78+
// - The index-signature annotation stops tsc from inferring the
79+
// PropTypes.Requireable<...> types and emitting `import PropTypes from
80+
// 'prop-types'` into the generated public .d.ts, which would force consumers
81+
// to install @types/prop-types (the v4.5.0 hand-written typings had none).
82+
// - The `?` keeps `propTypes` from being a *required* member of the public
83+
// type. React <= 18's JSX LibraryManagedAttributes only consults a
84+
// component's `propTypes` when it is required (`C extends {propTypes: ...}`);
85+
// when it does, this index-signature `propTypes` makes `defaultProps` stop
86+
// marking props optional, so consumers are forced to pass every prop.
87+
// Optional dodges that branch; React 19 ignores `propTypes` entirely. The
88+
// typings/tsconfig.react18.json check guards against a regression here.
8189
// Do not remove. See lib/Draggable.tsx for the same guard.
82-
static propTypes: {[key: string]: unknown} = {
90+
static propTypes?: {[key: string]: unknown} = {
8391
/**
8492
* `allowAnyClick` allows dragging using any mouse button.
8593
* By default, we only accept the left button.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
"@types/node": "^25.0.3",
5858
"@types/prop-types": "^15.7.15",
5959
"@types/react": "^19.2.7",
60+
"@types/react-18": "npm:@types/react@^18.3.27",
6061
"@types/react-dom": "^19.2.3",
6162
"@typescript-eslint/eslint-plugin": "^8.18.0",
6263
"@typescript-eslint/parser": "^8.18.0",

typings/tsconfig.react18.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"//": "Type-checks the public API against @types/react@18 as well as the default v19. React 18's JSX `LibraryManagedAttributes` consults a component's `propTypes`, unlike v19 which ignores it. A `propTypes` member leaking into the public types there makes `defaultProps` stop marking props optional, so React 18 consumers must pass every prop. v19 alone cannot catch that regression. Only `react` is redirected to the v18 types; `react-dom` stays on its installed v19 types (skipLibCheck ignores any v18/v19 mismatch inside its .d.ts). Aliasing react-dom too would pull in an `@types/react-dom-18` whose `@types/react@^18` peer cannot be satisfied under npm's strict resolution.",
3+
"compilerOptions": {
4+
"noEmit": true,
5+
"jsx": "preserve",
6+
"strict": true,
7+
"target": "ES2019",
8+
"lib": ["ES2019", "DOM", "DOM.Iterable"],
9+
"moduleResolution": "node",
10+
"esModuleInterop": true,
11+
"baseUrl": ".",
12+
"types": ["node"],
13+
"skipLibCheck": true,
14+
"paths": {
15+
"react-draggable": ["../lib/cjs.ts"],
16+
"react": ["../node_modules/@types/react-18"],
17+
"react/jsx-runtime": ["../node_modules/@types/react-18/jsx-runtime"]
18+
}
19+
},
20+
"files": [
21+
"test.tsx"
22+
]
23+
}

0 commit comments

Comments
 (0)