From 6a036914eae46748baed12983b51e63bf1906837 Mon Sep 17 00:00:00 2001 From: Matias Simon Date: Wed, 16 Sep 2026 18:52:06 +0200 Subject: [PATCH 1/2] Type clickable and hiddenInputContainer as what they accept The option types are derived from `defaultOptions`, so each one is inferred from its default value. `clickable: true` came out as `boolean` and `hiddenInputContainer: "body"` as `string`, neither of which matches what the option documents two lines above it or what Dropzone has always accepted: `clickable` is passed to `getElements`, which takes an element, a CSS selector or an array of those, and `hiddenInputContainer` to `getElement`, which takes an element or a selector. Widen both the way `previewsContainer` already is. This is a declaration fix only -- the unit tests already cover `clickable` as `true`, as a CSS selector, as an element and as an array, and they are untouched. Fixes #2373 --- .changeset/clickable-option-type.md | 9 +++++++++ packages/dropzone/src/options.ts | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) create mode 100644 .changeset/clickable-option-type.md diff --git a/.changeset/clickable-option-type.md b/.changeset/clickable-option-type.md new file mode 100644 index 000000000..db1d55f1e --- /dev/null +++ b/.changeset/clickable-option-type.md @@ -0,0 +1,9 @@ +--- +"dropzone": patch +--- + +Fix the declared type of `clickable` and `hiddenInputContainer`. + +Both options are derived from their default values, so `clickable: true` was inferred as `boolean` and `hiddenInputContainer: "body"` as `string`. That contradicted what each one documents and what both have always accepted at runtime, and TypeScript rejected the documented forms. + +`clickable` is now `boolean | string | HTMLElement | (string | HTMLElement)[]` and `hiddenInputContainer` is `string | HTMLElement`. Nothing changes at runtime. diff --git a/packages/dropzone/src/options.ts b/packages/dropzone/src/options.ts index 84df83729..a6e714b04 100644 --- a/packages/dropzone/src/options.ts +++ b/packages/dropzone/src/options.ts @@ -203,7 +203,7 @@ let defaultOptions = { * or an array of those. In that case, all of those elements will trigger an * upload when clicked. */ - clickable: true, + clickable: true as boolean | string | HTMLElement | (string | HTMLElement)[], /** * Whether hidden files in directories should be ignored. @@ -274,7 +274,7 @@ let defaultOptions = { * * Can be a selector string, or an element directly. */ - hiddenInputContainer: "body", + hiddenInputContainer: "body" as string | HTMLElement, /** * If null, no capture type will be specified From c8da4322746038f08bb7889161e1b627ca95972b Mon Sep 17 00:00:00 2001 From: Matias Simon Date: Wed, 16 Sep 2026 18:52:11 +0200 Subject: [PATCH 2/2] Check the public types as part of typecheck `DropzoneOptions` is derived from `defaultOptions`, so every option is typed by whatever its default happens to be. Where a default is narrower than what the option accepts -- `clickable: true` standing in for an element, a selector or an array of those -- the `as` widening in options.ts is the only thing keeping the declaration honest, and nothing in the suite would notice it being dropped. The unit tests cannot: they are JavaScript, and this is a compile-time property. Add test/types, checked by tsc and never emitted. It needs its own tsconfig because the build one is `emitDeclarationOnly` over src, so assertions living under src would ship to dist as declarations of their own; `pnpm build:types` still emits only the four src declarations. Reverting the widening in options.ts now fails `pnpm typecheck` with the four errors from #2373. The `@ts-expect-error` lines assert in the other direction: each one fails the build if it stops being an error, so they also pin the types against being widened to `any`. --- packages/dropzone/package.json | 2 +- .../dropzone/test/types/options.test-d.ts | 44 +++++++++++++++++++ packages/dropzone/tsconfig.typecheck.json | 13 ++++++ 3 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 packages/dropzone/test/types/options.test-d.ts create mode 100644 packages/dropzone/tsconfig.typecheck.json diff --git a/packages/dropzone/package.json b/packages/dropzone/package.json index 2926f97a0..b8df8532b 100644 --- a/packages/dropzone/package.json +++ b/packages/dropzone/package.json @@ -52,7 +52,7 @@ "build": "vite build && vite build --config vite.config.global.mjs && pnpm run css && pnpm run build:types", "css": "lightningcss --minify --sourcemap --browserslist src/dropzone.css -o dist/dropzone.css && lightningcss --minify --sourcemap --browserslist src/basic.css -o dist/basic.css", "build:types": "tsc", - "typecheck": "tsc --noEmit", + "typecheck": "tsc --noEmit -p tsconfig.typecheck.json", "test": "vitest run", "test:coverage": "vitest run --coverage", "test:watch": "vitest", diff --git a/packages/dropzone/test/types/options.test-d.ts b/packages/dropzone/test/types/options.test-d.ts new file mode 100644 index 000000000..a3b744dde --- /dev/null +++ b/packages/dropzone/test/types/options.test-d.ts @@ -0,0 +1,44 @@ +// Assertions about the option types, checked by `pnpm typecheck` and never +// emitted -- see tsconfig.typecheck.json. +// +// `DropzoneOptions` is derived from the default values, so an option is typed +// by whatever its default happens to be. Where a default is narrower than what +// the option accepts, the widening in options.ts is the only thing keeping the +// declaration honest, and nothing else would notice it being dropped. Hence +// these. A `@ts-expect-error` is itself an assertion: it fails the build if the +// line it marks stops being an error, so it also pins the type against being +// widened all the way to `any`. +import type { DropzoneOptions } from "../../src/options"; + +let element: HTMLElement = document.createElement("div"); + +// clickable: true for the dropzone element, false for nothing, or an element, +// a CSS selector, or an array of those. See #2373. +export const clickableTrue: DropzoneOptions = { clickable: true }; +export const clickableFalse: DropzoneOptions = { clickable: false }; +export const clickableElement: DropzoneOptions = { clickable: element }; +export const clickableSelector: DropzoneOptions = { clickable: ".pick-files" }; +export const clickableList: DropzoneOptions = { clickable: [element, ".pick-files"] }; + +// @ts-expect-error clickable is not a number +export const clickableNumber: DropzoneOptions = { clickable: 42 }; + +// hiddenInputContainer: a selector string or an element. +export const hiddenInputSelector: DropzoneOptions = { hiddenInputContainer: "body" }; +export const hiddenInputElement: DropzoneOptions = { hiddenInputContainer: element }; + +// @ts-expect-error hiddenInputContainer is not a number +export const hiddenInputNumber: DropzoneOptions = { hiddenInputContainer: 42 }; + +// previewsContainer: a selector, an element, or false to opt out. +export const previewsSelector: DropzoneOptions = { previewsContainer: ".previews" }; +export const previewsElement: DropzoneOptions = { previewsContainer: element }; +export const previewsFalse: DropzoneOptions = { previewsContainer: false }; + +// @ts-expect-error previewsContainer is not a number +export const previewsNumber: DropzoneOptions = { previewsContainer: 42 }; + +// The lack of an index signature is deliberate: a misspelled option has to be +// an error rather than an unused custom key. See the note on DropzoneOptions. +// @ts-expect-error maxFileSize is the wrong capitalisation of maxFilesize +export const misspelled: DropzoneOptions = { maxFileSize: 4 }; diff --git a/packages/dropzone/tsconfig.typecheck.json b/packages/dropzone/tsconfig.typecheck.json new file mode 100644 index 000000000..5ad41af08 --- /dev/null +++ b/packages/dropzone/tsconfig.typecheck.json @@ -0,0 +1,13 @@ +{ + // Type checking only, so it can reach files the build must not emit: + // test/types holds assertions about the public types, which would otherwise + // land in dist/ as declarations of their own. + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": true, + "declaration": false, + "emitDeclarationOnly": false, + "rootDir": "." + }, + "include": ["src/**/*.ts", "src/types.d.ts", "test/types/**/*.ts"] +}