Skip to content

Commit 87b3ffc

Browse files
authored
Ensure onChange types are contravariant instead of bivariant (#3781)
This PR fixes an issue where the types for certain event handlers such as `onChange` on the `Combobox` component were incorrectly typed as bivariant instead of contravariant. This is now fixed by using function property syntax instead of method syntax. Before: ```tsx function onChange(value: string) {} // This would have worked, even though the internal types are `(value: string | null) => void` return <Combobox onChange={onChange} />; ``` After: ```tsx function onChange(value: string) {} // This now errors because we expect `string | null` return <Combobox onChange={onChange} />; ``` In both cases: ```tsx function onChange(value: string | null) {} // This is the correct type, and works in both cases return <Combobox onChange={onChange} />; ``` A pure TypeScript example, thanks to @mn-prp: https://www.typescriptlang.org/play/?#code/MYewdgzgLgBMCGAbRB1AllAFgWQKZZABMYBeGACgAcAnESiALhgG8ZwBhTeMAc13IBuSAK64mYYchgAfGNGppeASiYCQaYgF8lpAHwtNAKEOhIsBMnRYACrUq5qUAJ6kYFGnUYs2YTtz5MgiJiMBJSsvKKPDok+moaMNp6Bsam0HBIiABG8MAA1q5BiKJMkcrJQsW4qeDp8NQ8rqwcXLwhFtm5BUYmmVY4+JhE5PXRvZYYmLZ0Ds4jDUpAA --- The `onChange` was the important one, but fixed the other spots where we use method syntax as well. Fixes: #3727
1 parent a4a0ea5 commit 87b3ffc

File tree

7 files changed

+13
-12
lines changed

7 files changed

+13
-12
lines changed

packages/@headlessui-react/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Fixed
1111

1212
- Ensure we are not freezing data when the `static` prop is used ([#3779](https://github.com/tailwindlabs/headlessui/pull/3779))
13+
- Ensure `onChange` types are contravariant instead of bivariant ([#3781](https://github.com/tailwindlabs/headlessui/pull/3781))
1314

1415
## [2.2.7] - 2025-07-30
1516

packages/@headlessui-react/src/components/combobox/combobox.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -256,9 +256,9 @@ export type ComboboxProps<
256256
value?: TMultiple extends true ? EnsureArray<TValue> : TValue
257257
defaultValue?: TMultiple extends true ? EnsureArray<NoInfer<TValue>> : NoInfer<TValue>
258258

259-
onChange?(
259+
onChange?: (
260260
value: TMultiple extends true ? EnsureArray<NoInfer<TValue>> : NoInfer<TValue> | null
261-
): void
261+
) => void
262262
by?: ByComparator<
263263
TMultiple extends true ? EnsureArray<NoInfer<TValue>>[number] : NoInfer<TValue>
264264
>
@@ -279,7 +279,7 @@ export type ComboboxProps<
279279
) => boolean
280280
} | null
281281

282-
onClose?(): void
282+
onClose?: () => void
283283

284284
__demoMode?: boolean
285285
}
@@ -513,8 +513,8 @@ export type ComboboxInputProps<
513513
{
514514
defaultValue?: TType
515515
disabled?: boolean
516-
displayValue?(item: TType): string
517-
onChange?(event: React.ChangeEvent<HTMLInputElement>): void
516+
displayValue?: (item: TType) => string
517+
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void
518518
autoFocus?: boolean
519519
}
520520
>

packages/@headlessui-react/src/components/listbox/listbox.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ export type ListboxProps<
147147
{
148148
value?: TType
149149
defaultValue?: TType
150-
onChange?(value: TType): void
150+
onChange?: (value: TType) => void
151151
by?: ByComparator<TActualType>
152152
disabled?: boolean
153153
invalid?: boolean

packages/@headlessui-react/src/components/radio-group/radio-group.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,7 @@ export type RadioGroupProps<
156156
{
157157
value?: TType
158158
defaultValue?: TType
159-
onChange?(value: TType): void
159+
onChange?: (value: TType) => void
160160
by?: ByComparator<TType>
161161
disabled?: boolean
162162
form?: string

packages/@headlessui-react/src/components/switch/switch.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,7 +129,7 @@ export type SwitchProps<TTag extends ElementType = typeof DEFAULT_SWITCH_TAG> =
129129
{
130130
checked?: boolean
131131
defaultChecked?: boolean
132-
onChange?(checked: boolean): void
132+
onChange?: (checked: boolean) => void
133133
name?: string
134134
value?: string
135135
form?: string

packages/@headlessui-react/src/hooks/document-overflow/overflow-store.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,8 @@ export interface Context<MetaType extends Record<string, any> = any> {
2828
}
2929

3030
export interface ScrollLockStep<MetaType extends Record<string, any> = any> {
31-
before?(ctx: Context<MetaType>): void
32-
after?(ctx: Context<MetaType>): void
31+
before?: (ctx: Context<MetaType>) => void
32+
after?: (ctx: Context<MetaType>) => void
3333
}
3434

3535
export let overflows = createStore(() => new Map<Document, DocEntry>(), {

packages/@headlessui-react/src/hooks/use-transition.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,8 @@ export function useTransition(
8282
element: HTMLElement | null,
8383
show: boolean,
8484
events?: {
85-
start?(show: boolean): void
86-
end?(show: boolean): void
85+
start?: (show: boolean) => void
86+
end?: (show: boolean) => void
8787
}
8888
): [visible: boolean, data: TransitionData] {
8989
let [visible, setVisible] = useState(show)

0 commit comments

Comments
 (0)