Skip to content

Commit 1bfb3a0

Browse files
authored
[react] Properly type form-related events (DefinitelyTyped#74383)
1 parent b610055 commit 1bfb3a0

File tree

9 files changed

+179
-86
lines changed

9 files changed

+179
-86
lines changed

types/react-dom/test/react-dom-tests.tsx

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -700,3 +700,66 @@ function cacheSignalTest() {
700700
}
701701
}
702702
}
703+
704+
function formrelatedEventTests() {
705+
React.createElement("textarea", {
706+
value: "5",
707+
onChange: event => {
708+
// createElement is not inferring props for textarea. JSX works though.
709+
// $ExpectType EventTarget & HTMLElement
710+
event.target;
711+
// @ts-expect-error
712+
event.target.value;
713+
},
714+
});
715+
716+
React.createElement("input", {
717+
onChange: event => {
718+
// $ExpectType EventTarget & HTMLInputElement
719+
event.target;
720+
// $ExpectType string
721+
event.target.value;
722+
},
723+
});
724+
725+
<div
726+
onChange={event => {
727+
// Should be EventTarget since we don't know from where the change event bubbled.
728+
// $ExpectType EventTarget & HTMLDivElement
729+
event.target;
730+
}}
731+
/>;
732+
<input
733+
onChange={event => {
734+
// $ExpectType EventTarget & HTMLInputElement
735+
event.target;
736+
// $ExpectType string
737+
event.target.value;
738+
}}
739+
/>;
740+
<select
741+
onChange={event => {
742+
// $ExpectType EventTarget & HTMLSelectElement
743+
event.target;
744+
// $ExpectType string
745+
event.target.value;
746+
}}
747+
/>;
748+
<textarea
749+
onChange={event => {
750+
// $ExpectType EventTarget & HTMLTextAreaElement
751+
event.target;
752+
// $ExpectType string
753+
event.target.value;
754+
}}
755+
/>;
756+
757+
<form
758+
onSubmit={event => {
759+
// @ts-expect-error -- submitter is not yet exposed by React
760+
event.submitter;
761+
// $ExpectType EventTarget & HTMLFormElement
762+
event.target;
763+
}}
764+
/>;
765+
}

types/react/global.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ interface KeyboardEvent extends Event {}
1818
interface MouseEvent extends Event {}
1919
interface TouchEvent extends Event {}
2020
interface PointerEvent extends Event {}
21+
interface SubmitEvent extends Event {}
2122
interface ToggleEvent extends Event {}
2223
interface TransitionEvent extends Event {}
2324
interface UIEvent extends Event {}

types/react/index.d.ts

Lines changed: 54 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ type NativeKeyboardEvent = KeyboardEvent;
1616
type NativeMouseEvent = MouseEvent;
1717
type NativeTouchEvent = TouchEvent;
1818
type NativePointerEvent = PointerEvent;
19+
type NativeSubmitEvent = SubmitEvent;
1920
type NativeToggleEvent = ToggleEvent;
2021
type NativeTransitionEvent = TransitionEvent;
2122
type NativeUIEvent = UIEvent;
@@ -2065,15 +2066,28 @@ declare namespace React {
20652066
target: EventTarget & Target;
20662067
}
20672068

2069+
/**
2070+
* @deprecated FormEvent doesn't actually exist.
2071+
* You probably meant to use {@link ChangeEvent}, {@link InputEvent}, {@link SubmitEvent}, or just {@link SyntheticEvent} instead
2072+
* depending on the event type.
2073+
*/
20682074
interface FormEvent<T = Element> extends SyntheticEvent<T> {
20692075
}
20702076

20712077
interface InvalidEvent<T = Element> extends SyntheticEvent<T> {
2072-
target: EventTarget & T;
20732078
}
20742079

2075-
interface ChangeEvent<T = Element> extends SyntheticEvent<T> {
2076-
target: EventTarget & T;
2080+
/**
2081+
* change events bubble in React so their target is generally unknown.
2082+
* Only for form elements we know their target type because form events can't
2083+
* be nested.
2084+
* This type exists purely to narrow `target` for form elements. It doesn't
2085+
* reflect a DOM event. Change events are just fired as standard {@link SyntheticEvent}.
2086+
*/
2087+
interface ChangeEvent<CurrentTarget = Element, Target = Element> extends SyntheticEvent<CurrentTarget> {
2088+
// TODO: This is wrong for change event handlers on arbitrary. Should
2089+
// be EventTarget & Target, but kept for backward compatibility until React 20.
2090+
target: EventTarget & CurrentTarget;
20772091
}
20782092

20792093
interface InputEvent<T = Element> extends SyntheticEvent<T, NativeInputEvent> {
@@ -2143,6 +2157,13 @@ declare namespace React {
21432157
shiftKey: boolean;
21442158
}
21452159

2160+
interface SubmitEvent<T = Element> extends SyntheticEvent<T, NativeSubmitEvent> {
2161+
// Currently not exposed by Reat
2162+
// submitter: HTMLElement | null;
2163+
// SubmitEvents are always targetted at HTMLFormElements.
2164+
target: EventTarget & HTMLFormElement;
2165+
}
2166+
21462167
interface TouchEvent<T = Element> extends UIEvent<T, NativeTouchEvent> {
21472168
altKey: boolean;
21482169
changedTouches: TouchList;
@@ -2198,11 +2219,19 @@ declare namespace React {
21982219
type CompositionEventHandler<T = Element> = EventHandler<CompositionEvent<T>>;
21992220
type DragEventHandler<T = Element> = EventHandler<DragEvent<T>>;
22002221
type FocusEventHandler<T = Element> = EventHandler<FocusEvent<T>>;
2222+
/**
2223+
* @deprecated FormEventHandler doesn't actually exist.
2224+
* You probably meant to use {@link ChangeEventHandler}, {@link InputEventHandler}, {@link SubmitEventHandler}, or just {@link EventHandler} instead
2225+
* depending on the event type.
2226+
*/
22012227
type FormEventHandler<T = Element> = EventHandler<FormEvent<T>>;
2202-
type ChangeEventHandler<T = Element> = EventHandler<ChangeEvent<T>>;
2228+
type ChangeEventHandler<CurrentTarget = Element, Target = Element> = EventHandler<
2229+
ChangeEvent<CurrentTarget, Target>
2230+
>;
22032231
type InputEventHandler<T = Element> = EventHandler<InputEvent<T>>;
22042232
type KeyboardEventHandler<T = Element> = EventHandler<KeyboardEvent<T>>;
22052233
type MouseEventHandler<T = Element> = EventHandler<MouseEvent<T>>;
2234+
type SubmitEventHandler<T = Element> = EventHandler<SubmitEvent<T>>;
22062235
type TouchEventHandler<T = Element> = EventHandler<TouchEvent<T>>;
22072236
type PointerEventHandler<T = Element> = EventHandler<PointerEvent<T>>;
22082237
type UIEventHandler<T = Element> = EventHandler<UIEvent<T>>;
@@ -2256,19 +2285,19 @@ declare namespace React {
22562285
onBlur?: FocusEventHandler<T> | undefined;
22572286
onBlurCapture?: FocusEventHandler<T> | undefined;
22582287

2259-
// Form Events
2260-
onChange?: FormEventHandler<T> | undefined;
2261-
onChangeCapture?: FormEventHandler<T> | undefined;
2288+
// form related Events
2289+
onChange?: ChangeEventHandler<T> | undefined;
2290+
onChangeCapture?: ChangeEventHandler<T> | undefined;
22622291
onBeforeInput?: InputEventHandler<T> | undefined;
2263-
onBeforeInputCapture?: FormEventHandler<T> | undefined;
2264-
onInput?: FormEventHandler<T> | undefined;
2265-
onInputCapture?: FormEventHandler<T> | undefined;
2266-
onReset?: FormEventHandler<T> | undefined;
2267-
onResetCapture?: FormEventHandler<T> | undefined;
2268-
onSubmit?: FormEventHandler<T> | undefined;
2269-
onSubmitCapture?: FormEventHandler<T> | undefined;
2270-
onInvalid?: FormEventHandler<T> | undefined;
2271-
onInvalidCapture?: FormEventHandler<T> | undefined;
2292+
onBeforeInputCapture?: InputEventHandler<T> | undefined;
2293+
onInput?: InputEventHandler<T> | undefined;
2294+
onInputCapture?: InputEventHandler<T> | undefined;
2295+
onReset?: ReactEventHandler<T> | undefined;
2296+
onResetCapture?: ReactEventHandler<T> | undefined;
2297+
onSubmit?: SubmitEventHandler<T> | undefined;
2298+
onSubmitCapture?: SubmitEventHandler<T> | undefined;
2299+
onInvalid?: ReactEventHandler<T> | undefined;
2300+
onInvalidCapture?: ReactEventHandler<T> | undefined;
22722301

22732302
// Image Events
22742303
onLoad?: ReactEventHandler<T> | undefined;
@@ -3275,7 +3304,9 @@ declare namespace React {
32753304
value?: string | readonly string[] | number | undefined;
32763305
width?: number | string | undefined;
32773306

3278-
onChange?: ChangeEventHandler<T> | undefined;
3307+
// No other element dispatching change events can be nested in a <input>
3308+
// so we know the target will be a HTMLInputElement.
3309+
onChange?: ChangeEventHandler<T, HTMLInputElement> | undefined;
32793310
}
32803311

32813312
interface KeygenHTMLAttributes<T> extends HTMLAttributes<T> {
@@ -3440,7 +3471,9 @@ declare namespace React {
34403471
required?: boolean | undefined;
34413472
size?: number | undefined;
34423473
value?: string | readonly string[] | number | undefined;
3443-
onChange?: ChangeEventHandler<T> | undefined;
3474+
// No other element dispatching change events can be nested in a <select>
3475+
// so we know the target will be a HTMLSelectElement.
3476+
onChange?: ChangeEventHandler<T, HTMLSelectElement> | undefined;
34443477
}
34453478

34463479
interface SourceHTMLAttributes<T> extends HTMLAttributes<T> {
@@ -3492,7 +3525,9 @@ declare namespace React {
34923525
value?: string | readonly string[] | number | undefined;
34933526
wrap?: string | undefined;
34943527

3495-
onChange?: ChangeEventHandler<T> | undefined;
3528+
// No other element dispatching change events can be nested in a <textare>
3529+
// so we know the target will be a HTMLTextAreaElement.
3530+
onChange?: ChangeEventHandler<T, HTMLTextAreaElement> | undefined;
34963531
}
34973532

34983533
interface TdHTMLAttributes<T> extends HTMLAttributes<T> {

types/react/test/index.ts

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -633,25 +633,6 @@ function handler(e: React.MouseEvent) {
633633

634634
const keyboardExtendsUI: React.UIEventHandler = (e: React.KeyboardEvent) => {};
635635

636-
//
637-
// The SyntheticEvent.target.value should be accessible for onChange
638-
// --------------------------------------------------------------------------
639-
class SyntheticEventTargetValue extends React.Component<{}, { value: string }> {
640-
state: { value: string };
641-
constructor(props: {}) {
642-
super(props);
643-
this.state = { value: "a" };
644-
}
645-
render() {
646-
return React.createElement("textarea", {
647-
value: this.state.value,
648-
onChange: e => {
649-
const target: HTMLTextAreaElement = e.target;
650-
},
651-
});
652-
}
653-
}
654-
655636
React.createElement("input", {
656637
onChange: event => {
657638
// `event.target` is guaranteed to be HTMLInputElement

types/react/ts5.0/global.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ interface KeyboardEvent extends Event {}
1818
interface MouseEvent extends Event {}
1919
interface TouchEvent extends Event {}
2020
interface PointerEvent extends Event {}
21+
interface SubmitEvent extends Event {}
2122
interface ToggleEvent extends Event {}
2223
interface TransitionEvent extends Event {}
2324
interface UIEvent extends Event {}

types/react/ts5.0/index.d.ts

Lines changed: 54 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ type NativeKeyboardEvent = KeyboardEvent;
1616
type NativeMouseEvent = MouseEvent;
1717
type NativeTouchEvent = TouchEvent;
1818
type NativePointerEvent = PointerEvent;
19+
type NativeSubmitEvent = SubmitEvent;
1920
type NativeToggleEvent = ToggleEvent;
2021
type NativeTransitionEvent = TransitionEvent;
2122
type NativeUIEvent = UIEvent;
@@ -2064,15 +2065,28 @@ declare namespace React {
20642065
target: EventTarget & Target;
20652066
}
20662067

2068+
/**
2069+
* @deprecated FormEvent doesn't actually exist.
2070+
* You probably meant to use {@link ChangeEvent}, {@link InputEvent}, {@link SubmitEvent}, or just {@link SyntheticEvent} instead
2071+
* depending on the event type.
2072+
*/
20672073
interface FormEvent<T = Element> extends SyntheticEvent<T> {
20682074
}
20692075

20702076
interface InvalidEvent<T = Element> extends SyntheticEvent<T> {
2071-
target: EventTarget & T;
20722077
}
20732078

2074-
interface ChangeEvent<T = Element> extends SyntheticEvent<T> {
2075-
target: EventTarget & T;
2079+
/**
2080+
* change events bubble in React so their target is generally unknown.
2081+
* Only for form elements we know their target type because form events can't
2082+
* be nested.
2083+
* This type exists purely to narrow `target` for form elements. It doesn't
2084+
* reflect a DOM event. React fires change events as {@link SyntheticEvent}.
2085+
*/
2086+
interface ChangeEvent<CurrentTarget = Element, Target = Element> extends SyntheticEvent<CurrentTarget> {
2087+
// TODO: This is wrong for change event handlers on arbitrary. Should
2088+
// be EventTarget & Target, but kept for backward compatibility until React 20.
2089+
target: EventTarget & CurrentTarget;
20762090
}
20772091

20782092
interface InputEvent<T = Element> extends SyntheticEvent<T, NativeInputEvent> {
@@ -2142,6 +2156,13 @@ declare namespace React {
21422156
shiftKey: boolean;
21432157
}
21442158

2159+
interface SubmitEvent<T = Element> extends SyntheticEvent<T, NativeSubmitEvent> {
2160+
// Currently not exposed by Reat
2161+
// submitter: HTMLElement | null;
2162+
// SubmitEvents are always targetted at HTMLFormElements.
2163+
target: EventTarget & HTMLFormElement;
2164+
}
2165+
21452166
interface TouchEvent<T = Element> extends UIEvent<T, NativeTouchEvent> {
21462167
altKey: boolean;
21472168
changedTouches: TouchList;
@@ -2197,11 +2218,19 @@ declare namespace React {
21972218
type CompositionEventHandler<T = Element> = EventHandler<CompositionEvent<T>>;
21982219
type DragEventHandler<T = Element> = EventHandler<DragEvent<T>>;
21992220
type FocusEventHandler<T = Element> = EventHandler<FocusEvent<T>>;
2221+
/**
2222+
* @deprecated FormEventHandler doesn't actually exist.
2223+
* You probably meant to use {@link ChangeEventHandler}, {@link InputEventHandler}, {@link SubmitEventHandler}, or just {@link EventHandler} instead
2224+
* depending on the event type.
2225+
*/
22002226
type FormEventHandler<T = Element> = EventHandler<FormEvent<T>>;
2201-
type ChangeEventHandler<T = Element> = EventHandler<ChangeEvent<T>>;
2227+
type ChangeEventHandler<CurrentTarget = Element, Target = Element> = EventHandler<
2228+
ChangeEvent<CurrentTarget, Target>
2229+
>;
22022230
type InputEventHandler<T = Element> = EventHandler<InputEvent<T>>;
22032231
type KeyboardEventHandler<T = Element> = EventHandler<KeyboardEvent<T>>;
22042232
type MouseEventHandler<T = Element> = EventHandler<MouseEvent<T>>;
2233+
type SubmitEventHandler<T = Element> = EventHandler<SubmitEvent<T>>;
22052234
type TouchEventHandler<T = Element> = EventHandler<TouchEvent<T>>;
22062235
type PointerEventHandler<T = Element> = EventHandler<PointerEvent<T>>;
22072236
type UIEventHandler<T = Element> = EventHandler<UIEvent<T>>;
@@ -2255,19 +2284,19 @@ declare namespace React {
22552284
onBlur?: FocusEventHandler<T> | undefined;
22562285
onBlurCapture?: FocusEventHandler<T> | undefined;
22572286

2258-
// Form Events
2259-
onChange?: FormEventHandler<T> | undefined;
2260-
onChangeCapture?: FormEventHandler<T> | undefined;
2287+
// form related Events
2288+
onChange?: ChangeEventHandler<T> | undefined;
2289+
onChangeCapture?: ChangeEventHandler<T> | undefined;
22612290
onBeforeInput?: InputEventHandler<T> | undefined;
2262-
onBeforeInputCapture?: FormEventHandler<T> | undefined;
2263-
onInput?: FormEventHandler<T> | undefined;
2264-
onInputCapture?: FormEventHandler<T> | undefined;
2265-
onReset?: FormEventHandler<T> | undefined;
2266-
onResetCapture?: FormEventHandler<T> | undefined;
2267-
onSubmit?: FormEventHandler<T> | undefined;
2268-
onSubmitCapture?: FormEventHandler<T> | undefined;
2269-
onInvalid?: FormEventHandler<T> | undefined;
2270-
onInvalidCapture?: FormEventHandler<T> | undefined;
2291+
onBeforeInputCapture?: InputEventHandler<T> | undefined;
2292+
onInput?: InputEventHandler<T> | undefined;
2293+
onInputCapture?: InputEventHandler<T> | undefined;
2294+
onReset?: ReactEventHandler<T> | undefined;
2295+
onResetCapture?: ReactEventHandler<T> | undefined;
2296+
onSubmit?: SubmitEventHandler<T> | undefined;
2297+
onSubmitCapture?: SubmitEventHandler<T> | undefined;
2298+
onInvalid?: ReactEventHandler<T> | undefined;
2299+
onInvalidCapture?: ReactEventHandler<T> | undefined;
22712300

22722301
// Image Events
22732302
onLoad?: ReactEventHandler<T> | undefined;
@@ -3274,7 +3303,9 @@ declare namespace React {
32743303
value?: string | readonly string[] | number | undefined;
32753304
width?: number | string | undefined;
32763305

3277-
onChange?: ChangeEventHandler<T> | undefined;
3306+
// No other element dispatching change events can be nested in a <input>
3307+
// so we know the target will be a HTMLInputElement.
3308+
onChange?: ChangeEventHandler<T, HTMLInputElement> | undefined;
32783309
}
32793310

32803311
interface KeygenHTMLAttributes<T> extends HTMLAttributes<T> {
@@ -3439,7 +3470,9 @@ declare namespace React {
34393470
required?: boolean | undefined;
34403471
size?: number | undefined;
34413472
value?: string | readonly string[] | number | undefined;
3442-
onChange?: ChangeEventHandler<T> | undefined;
3473+
// No other element dispatching change events can be nested in a <select>
3474+
// so we know the target will be a HTMLSelectElement.
3475+
onChange?: ChangeEventHandler<T, HTMLSelectElement> | undefined;
34433476
}
34443477

34453478
interface SourceHTMLAttributes<T> extends HTMLAttributes<T> {
@@ -3491,7 +3524,9 @@ declare namespace React {
34913524
value?: string | readonly string[] | number | undefined;
34923525
wrap?: string | undefined;
34933526

3494-
onChange?: ChangeEventHandler<T> | undefined;
3527+
// No other element dispatching change events can be nested in a <textarea>
3528+
// so we know the target will be a HTMLTextAreaElement.
3529+
onChange?: ChangeEventHandler<T, HTMLTextAreaElement> | undefined;
34953530
}
34963531

34973532
interface TdHTMLAttributes<T> extends HTMLAttributes<T> {

0 commit comments

Comments
 (0)