From 93e4410562ee79749967a83af75f5283cd99ae7d Mon Sep 17 00:00:00 2001 From: Moti Zilberman Date: Sat, 11 Aug 2018 18:52:29 +0100 Subject: [PATCH 01/10] Create placeholder prop types for react-dom HTML/SVG intrinsics --- lib/react-dom.js | 110 ++++++++++++++++++++++++++--------------------- 1 file changed, 62 insertions(+), 48 deletions(-) diff --git a/lib/react-dom.js b/lib/react-dom.js index 59761671c81..e2aa5e6a671 100644 --- a/lib/react-dom.js +++ b/lib/react-dom.js @@ -272,13 +272,13 @@ declare class SyntheticTransitionEvent< // prettier-ignore declare type $JSXIntrinsics = { // HTML - a: {instance: HTMLAnchorElement, props: {children?: React$Node, [key: string]: any}}, + a: {instance: HTMLAnchorElement, props: ReactDOM$HTMLElementProps}, abbr: ReactDOM$HTMLElementJSXIntrinsic, address: ReactDOM$HTMLElementJSXIntrinsic, area: ReactDOM$HTMLElementJSXIntrinsic, article: ReactDOM$HTMLElementJSXIntrinsic, aside: ReactDOM$HTMLElementJSXIntrinsic, - audio: {instance: HTMLAudioElement, props: {children?: React$Node, [key: string]: any}}, + audio: {instance: HTMLAudioElement, props: ReactDOM$HTMLElementProps}, b: ReactDOM$HTMLElementJSXIntrinsic, base: ReactDOM$HTMLElementJSXIntrinsic, bdi: ReactDOM$HTMLElementJSXIntrinsic, @@ -286,10 +286,10 @@ declare type $JSXIntrinsics = { big: ReactDOM$HTMLElementJSXIntrinsic, blockquote: ReactDOM$HTMLElementJSXIntrinsic, body: ReactDOM$HTMLElementJSXIntrinsic, - br: {instance: HTMLBRElement, props: {children?: React$Node, [key: string]: any}}, - button: {instance: HTMLButtonElement, props: {children?: React$Node, [key: string]: any}}, - canvas: {instance: HTMLCanvasElement, props: {children?: React$Node, [key: string]: any}}, - caption: {instance: HTMLTableCaptionElement, props: {children?: React$Node, [key: string]: any}}, + br: {instance: HTMLBRElement, props: ReactDOM$HTMLElementProps}, + button: {instance: HTMLButtonElement, props: ReactDOM$HTMLElementProps}, + canvas: {instance: HTMLCanvasElement, props: ReactDOM$HTMLElementProps}, + caption: {instance: HTMLTableCaptionElement, props: ReactDOM$HTMLElementProps}, cite: ReactDOM$HTMLElementJSXIntrinsic, code: ReactDOM$HTMLElementJSXIntrinsic, col: ReactDOM$HTMLElementJSXIntrinsic, @@ -298,58 +298,58 @@ declare type $JSXIntrinsics = { datalist: ReactDOM$HTMLElementJSXIntrinsic, dd: ReactDOM$HTMLElementJSXIntrinsic, del: ReactDOM$HTMLElementJSXIntrinsic, - details: {instance: HTMLDetailsElement, props: {children?: React$Node, [key: string]: any}}, + details: {instance: HTMLDetailsElement, props: ReactDOM$HTMLElementProps}, dfn: ReactDOM$HTMLElementJSXIntrinsic, dialog: ReactDOM$HTMLElementJSXIntrinsic, - div: {instance: HTMLDivElement, props: {children?: React$Node, [key: string]: any}}, - dl: {instance: HTMLDListElement, props: {children?: React$Node, [key: string]: any}}, + div: {instance: HTMLDivElement, props: ReactDOM$HTMLElementProps}, + dl: {instance: HTMLDListElement, props: ReactDOM$HTMLElementProps}, dt: ReactDOM$HTMLElementJSXIntrinsic, em: ReactDOM$HTMLElementJSXIntrinsic, embed: ReactDOM$HTMLElementJSXIntrinsic, - fieldset: {instance: HTMLFieldSetElement, props: {children?: React$Node, [key: string]: any}}, + fieldset: {instance: HTMLFieldSetElement, props: ReactDOM$HTMLElementProps}, figcaption: ReactDOM$HTMLElementJSXIntrinsic, figure: ReactDOM$HTMLElementJSXIntrinsic, footer: ReactDOM$HTMLElementJSXIntrinsic, - form: {instance: HTMLFormElement, props: {children?: React$Node, [key: string]: any}}, - h1: {instance: HTMLHeadingElement, props: {children?: React$Node, [key: string]: any}}, - h2: {instance: HTMLHeadingElement, props: {children?: React$Node, [key: string]: any}}, - h3: {instance: HTMLHeadingElement, props: {children?: React$Node, [key: string]: any}}, - h4: {instance: HTMLHeadingElement, props: {children?: React$Node, [key: string]: any}}, - h5: {instance: HTMLHeadingElement, props: {children?: React$Node, [key: string]: any}}, - h6: {instance: HTMLHeadingElement, props: {children?: React$Node, [key: string]: any}}, + form: {instance: HTMLFormElement, props: ReactDOM$HTMLElementProps}, + h1: {instance: HTMLHeadingElement, props: ReactDOM$HTMLElementProps}, + h2: {instance: HTMLHeadingElement, props: ReactDOM$HTMLElementProps}, + h3: {instance: HTMLHeadingElement, props: ReactDOM$HTMLElementProps}, + h4: {instance: HTMLHeadingElement, props: ReactDOM$HTMLElementProps}, + h5: {instance: HTMLHeadingElement, props: ReactDOM$HTMLElementProps}, + h6: {instance: HTMLHeadingElement, props: ReactDOM$HTMLElementProps}, head: ReactDOM$HTMLElementJSXIntrinsic, header: ReactDOM$HTMLElementJSXIntrinsic, hgroup: ReactDOM$HTMLElementJSXIntrinsic, - hr: {instance: HTMLHRElement, props: {children?: React$Node, [key: string]: any}}, + hr: {instance: HTMLHRElement, props: ReactDOM$HTMLElementProps}, html: ReactDOM$HTMLElementJSXIntrinsic, i: ReactDOM$HTMLElementJSXIntrinsic, - iframe: {instance: HTMLIFrameElement, props: {children?: React$Node, [key: string]: any}}, - img: {instance: HTMLImageElement, props: {children?: React$Node, [key: string]: any}}, + iframe: {instance: HTMLIFrameElement, props: ReactDOM$HTMLElementProps}, + img: {instance: HTMLImageElement, props: ReactDOM$HTMLElementProps}, ins: ReactDOM$HTMLElementJSXIntrinsic, kbd: ReactDOM$HTMLElementJSXIntrinsic, keygen: ReactDOM$HTMLElementJSXIntrinsic, - label: {instance: HTMLLabelElement, props: {children?: React$Node, [key: string]: any}}, - legend: {instance: HTMLLegendElement, props: {children?: React$Node, [key: string]: any}}, - li: {instance: HTMLLIElement, props: {children?: React$Node, [key: string]: any}}, - link: {instance: HTMLLinkElement, props: {children?: React$Node, [key: string]: any}}, + label: {instance: HTMLLabelElement, props: ReactDOM$HTMLElementProps}, + legend: {instance: HTMLLegendElement, props: ReactDOM$HTMLElementProps}, + li: {instance: HTMLLIElement, props: ReactDOM$HTMLElementProps}, + link: {instance: HTMLLinkElement, props: ReactDOM$HTMLElementProps}, main: ReactDOM$HTMLElementJSXIntrinsic, map: ReactDOM$HTMLElementJSXIntrinsic, mark: ReactDOM$HTMLElementJSXIntrinsic, menu: ReactDOM$HTMLElementJSXIntrinsic, menuitem: ReactDOM$HTMLElementJSXIntrinsic, - meta: {instance: HTMLMetaElement, props: {children?: React$Node, [key: string]: any}}, + meta: {instance: HTMLMetaElement, props: ReactDOM$HTMLElementProps}, meter: ReactDOM$HTMLElementJSXIntrinsic, nav: ReactDOM$HTMLElementJSXIntrinsic, noscript: ReactDOM$HTMLElementJSXIntrinsic, object: ReactDOM$HTMLElementJSXIntrinsic, - ol: {instance: HTMLOListElement, props: {children?: React$Node, [key: string]: any}}, - optgroup: {instance: HTMLOptGroupElement, props: {children?: React$Node, [key: string]: any}}, - option: {instance: HTMLOptionElement, props: {children?: React$Node, [key: string]: any}}, + ol: {instance: HTMLOListElement, props: ReactDOM$HTMLElementProps}, + optgroup: {instance: HTMLOptGroupElement, props: ReactDOM$HTMLElementProps}, + option: {instance: HTMLOptionElement, props: ReactDOM$HTMLElementProps}, output: ReactDOM$HTMLElementJSXIntrinsic, - p: {instance: HTMLParagraphElement, props: {children?: React$Node, [key: string]: any}}, + p: {instance: HTMLParagraphElement, props: ReactDOM$HTMLElementProps}, param: ReactDOM$HTMLElementJSXIntrinsic, picture: ReactDOM$HTMLElementJSXIntrinsic, - pre: {instance: HTMLPreElement, props: {children?: React$Node, [key: string]: any}}, + pre: {instance: HTMLPreElement, props: ReactDOM$HTMLElementProps}, progress: ReactDOM$HTMLElementJSXIntrinsic, q: ReactDOM$HTMLElementJSXIntrinsic, rp: ReactDOM$HTMLElementJSXIntrinsic, @@ -357,30 +357,30 @@ declare type $JSXIntrinsics = { ruby: ReactDOM$HTMLElementJSXIntrinsic, s: ReactDOM$HTMLElementJSXIntrinsic, samp: ReactDOM$HTMLElementJSXIntrinsic, - script: {instance: HTMLScriptElement, props: {children?: React$Node, [key: string]: any}}, + script: {instance: HTMLScriptElement, props: ReactDOM$HTMLElementProps}, section: ReactDOM$HTMLElementJSXIntrinsic, small: ReactDOM$HTMLElementJSXIntrinsic, - source: {instance: HTMLSourceElement, props: {children?: React$Node, [key: string]: any}}, - span: {instance: HTMLSpanElement, props: {children?: React$Node, [key: string]: any}}, + source: {instance: HTMLSourceElement, props: ReactDOM$HTMLElementProps}, + span: {instance: HTMLSpanElement, props: ReactDOM$HTMLElementProps}, strong: ReactDOM$HTMLElementJSXIntrinsic, - style: {instance: HTMLStyleElement, props: {children?: React$Node, [key: string]: any}}, + style: {instance: HTMLStyleElement, props: ReactDOM$HTMLElementProps}, sub: ReactDOM$HTMLElementJSXIntrinsic, summary: ReactDOM$HTMLElementJSXIntrinsic, sup: ReactDOM$HTMLElementJSXIntrinsic, - table: {instance: HTMLTableElement, props: {children?: React$Node, [key: string]: any}}, - tbody: {instance: HTMLTableSectionElement, props: {children?: React$Node, [key: string]: any}}, - td: {instance: HTMLTableCellElement, props: {children?: React$Node, [key: string]: any}}, - tfoot: {instance: HTMLTableSectionElement, props: {children?: React$Node, [key: string]: any}}, - th: {instance: HTMLTableCellElement, props: {children?: React$Node, [key: string]: any}}, - thead: {instance: HTMLTableSectionElement, props: {children?: React$Node, [key: string]: any}}, + table: {instance: HTMLTableElement, props: ReactDOM$HTMLElementProps}, + tbody: {instance: HTMLTableSectionElement, props: ReactDOM$HTMLElementProps}, + td: {instance: HTMLTableCellElement, props: ReactDOM$HTMLElementProps}, + tfoot: {instance: HTMLTableSectionElement, props: ReactDOM$HTMLElementProps}, + th: {instance: HTMLTableCellElement, props: ReactDOM$HTMLElementProps}, + thead: {instance: HTMLTableSectionElement, props: ReactDOM$HTMLElementProps}, time: ReactDOM$HTMLElementJSXIntrinsic, title: ReactDOM$HTMLElementJSXIntrinsic, - tr: {instance: HTMLTableRowElement, props: {children?: React$Node, [key: string]: any}}, + tr: {instance: HTMLTableRowElement, props: ReactDOM$HTMLElementProps}, track: ReactDOM$HTMLElementJSXIntrinsic, u: ReactDOM$HTMLElementJSXIntrinsic, - ul: {instance: HTMLUListElement, props: {children?: React$Node, [key: string]: any}}, + ul: {instance: HTMLUListElement, props: ReactDOM$HTMLElementProps}, 'var': ReactDOM$HTMLElementJSXIntrinsic, - video: {instance: HTMLVideoElement, props: {children?: React$Node, [key: string]: any}}, + video: {instance: HTMLVideoElement, props: ReactDOM$HTMLElementProps}, wbr: ReactDOM$HTMLElementJSXIntrinsic, // SVG svg: ReactDOM$SVGElementJSXIntrinsic, @@ -405,19 +405,33 @@ declare type $JSXIntrinsics = { tspan: ReactDOM$SVGElementJSXIntrinsic, use: ReactDOM$SVGElementJSXIntrinsic, // Elements React adds extra props for. - input: {instance: HTMLInputElement, props: {children?: React$Node, [key: string]: any}}, - textarea: {instance: HTMLTextAreaElement, props: {children?: React$Node, [key: string]: any}}, - select: {instance: HTMLSelectElement, props: {children?: React$Node, [key: string]: any}}, + input: {instance: HTMLInputElement, props: ReactDOM$HTMLElementProps}, + textarea: {instance: HTMLTextAreaElement, props: ReactDOM$HTMLElementProps}, + select: {instance: HTMLSelectElement, props: ReactDOM$HTMLElementProps}, // Catch-all for custom elements. [string]: ReactDOM$HTMLElementJSXIntrinsic, }; +type ReactDOM$GlobalEventHandlers = { + // TODO +}; + +type ReactDOM$ElementProps = { + [key: string]: any +}; + +type ReactDOM$HTMLElementProps = ReactDOM$ElementProps & ReactDOM$GlobalEventHandlers & { +}; + +type ReactDOM$SVGElementProps = ReactDOM$ElementProps & { +}; + type ReactDOM$HTMLElementJSXIntrinsic = { instance: HTMLElement, - props: {children?: React$Node, [key: string]: any}, + props: ReactDOM$HTMLElementProps, }; type ReactDOM$SVGElementJSXIntrinsic = { instance: Element, - props: {children?: React$Node, [key: string]: any}, + props: ReactDOM$SVGElementProps, }; From d9f35351e23508e818fd04e855d2bfa702d18696 Mon Sep 17 00:00:00 2001 From: Moti Zilberman Date: Sat, 11 Aug 2018 19:19:20 +0100 Subject: [PATCH 02/10] Type some props of React DOM intrinsics This is a first pass to start surfacing the architectural and practical issues involved in typing React DOM more precisely. In this commit: - No event props yet! - React global attributes like dangerouslySetInnerHTML - Shared HTML/SVG attributes - HTML global attributes - A handful of SVG global attributes - Lots of things marked TODO - Some design choices described in comments "Booleanish string" as a term is lifted from the React DOM source. --- lib/react-dom.js | 71 ++++ tests/jsx_intrinsics.builtin/strings.js | 2 +- tests/react_dom/.flowconfig | 2 + tests/react_dom/dangerouslySetInnerHTML.js | 8 + tests/react_dom/props_boolean.js | 15 + tests/react_dom/props_booleanish_string.js | 12 + tests/react_dom/props_enum.js | 15 + tests/react_dom/props_number.js | 11 + tests/react_dom/props_string.js | 6 + tests/react_dom/props_style.js | 6 + tests/react_dom/react_dom.exp | 418 +++++++++++++++++++++ tests/react_dom/suppress_warnings.js | 11 + 12 files changed, 576 insertions(+), 1 deletion(-) create mode 100644 tests/react_dom/.flowconfig create mode 100644 tests/react_dom/dangerouslySetInnerHTML.js create mode 100644 tests/react_dom/props_boolean.js create mode 100644 tests/react_dom/props_booleanish_string.js create mode 100644 tests/react_dom/props_enum.js create mode 100644 tests/react_dom/props_number.js create mode 100644 tests/react_dom/props_string.js create mode 100644 tests/react_dom/props_style.js create mode 100644 tests/react_dom/react_dom.exp create mode 100644 tests/react_dom/suppress_warnings.js diff --git a/lib/react-dom.js b/lib/react-dom.js index e2aa5e6a671..82963b054c4 100644 --- a/lib/react-dom.js +++ b/lib/react-dom.js @@ -417,15 +417,76 @@ type ReactDOM$GlobalEventHandlers = { }; type ReactDOM$ElementProps = { + children?: React$Node, + className?: string, + dangerouslySetInnerHTML?: { __html: string }, + id?: string, + lang?: string, + style?: ReactDOM$Style, + suppressHydrationWarning?: boolean, + tabIndex?: ReactDOM$Number, + + // NOTE: There is no way we can enumerate all acceptable/standard props of DOM + // intrinsics. If we ever got a Flow feature allowing regexes for keys, we + // could express open-ended namespaces like data-* attributes, custom + // attributes etc more precisely, and then maybe think about making this + // definition complete/exact (thus detecting more classes of errors). + // + // For now the typing philosophy here is: be reasonably strict with the types + // of known attributes, but lenient otherwise. + [key: string]: any }; type ReactDOM$HTMLElementProps = ReactDOM$ElementProps & ReactDOM$GlobalEventHandlers & { + accessKey?: string, + contentEditable?: ReactDOM$BooleanishString, + contextMenu?: string, + dir?: $TODO$CaseInsensitive<"ltr" | "rtl" | "LTR" | "RTL" | "auto">, + draggable?: ReactDOM$BooleanishString, + hidden?: ReactDOM$Boolean<"hidden">, + is?: string, + itemProp?: string, + itemRef?: string, + itemScope?: ReactDOM$Boolean<"itemScope" | "itemscope">, + itemType?: string, + slot?: string, + spellCheck?: ReactDOM$BooleanishString, + suppressContentEditableWarning?: boolean, + title?: string, + translate?: $TODO$CaseInsensitive<"" | "yes" | "no"> + + // TODO: HTML event props }; type ReactDOM$SVGElementProps = ReactDOM$ElementProps & { + xlinkActuate?: string, + xlinkArcrole?: string, + xlinkHref?: string, + xlinkRole?: string, + xlinkShow?: string, + xlinkTitle?: string, + xlinkType?: string, + xmlBase?: string, + xmlLang?: string, + xmlSpace?: string + + // TODO: SVG presentation props + // TODO: SVG filters props + // TODO: SVG animation props + // TODO: SVG event props }; +type ReactDOM$BooleanishString = + | "" + | $TODO$CaseInsensitive<"true" | "false"> + | boolean; + +type ReactDOM$Boolean = + | "" + | $TODO$CaseInsensitive + | boolean; + type ReactDOM$HTMLElementJSXIntrinsic = { instance: HTMLElement, props: ReactDOM$HTMLElementProps, @@ -435,3 +496,13 @@ type ReactDOM$SVGElementJSXIntrinsic = { instance: Element, props: ReactDOM$SVGElementProps, }; + +// TODO: Can we get an actual Flow $CaseInsensitive builtin for singleton +// strings and enums? +type $TODO$CaseInsensitive = Value; + +type ReactDOM$Style = Object; // TODO + +// NOTE: In principle this can be `number | string`, but constraining values to +// numbers might be a useful design choice. +type ReactDOM$Number = number; \ No newline at end of file diff --git a/tests/jsx_intrinsics.builtin/strings.js b/tests/jsx_intrinsics.builtin/strings.js index f63649cec5f..2f74b2dff6b 100644 --- a/tests/jsx_intrinsics.builtin/strings.js +++ b/tests/jsx_intrinsics.builtin/strings.js @@ -15,4 +15,4 @@ var Str: string = 'str'; React.createElement('div', {}); // This is fine React.createElement('bad', {}); // This is fine -
; // This is fine +
; // This is fine diff --git a/tests/react_dom/.flowconfig b/tests/react_dom/.flowconfig new file mode 100644 index 00000000000..de38d19537d --- /dev/null +++ b/tests/react_dom/.flowconfig @@ -0,0 +1,2 @@ +[options] +no_flowlib=false diff --git a/tests/react_dom/dangerouslySetInnerHTML.js b/tests/react_dom/dangerouslySetInnerHTML.js new file mode 100644 index 00000000000..196a4baae2a --- /dev/null +++ b/tests/react_dom/dangerouslySetInnerHTML.js @@ -0,0 +1,8 @@ +// @flow + +import React from "react"; + +
; // Error: Expected object +
; // Error: Expected object +
Hello" }} />; // OK +
Hello" }} />; // Error: Missing __html diff --git a/tests/react_dom/props_boolean.js b/tests/react_dom/props_boolean.js new file mode 100644 index 00000000000..eff01bd92c1 --- /dev/null +++ b/tests/react_dom/props_boolean.js @@ -0,0 +1,15 @@ +// @flow + +import React from "react"; + +