diff --git a/lib/util/propTypes.js b/lib/util/propTypes.js index 889d84596e..bb0804a10a 100644 --- a/lib/util/propTypes.js +++ b/lib/util/propTypes.js @@ -100,7 +100,7 @@ module.exports = function propTypesInstructions(context, components, utils) { const defaults = {customValidators: []}; const configuration = Object.assign({}, defaults, context.options[0] || {}); const customValidators = configuration.customValidators; - const allowedGenericTypes = new Set(['SFC', 'StatelessComponent', 'FunctionComponent', 'FC']); + const allowedGenericTypes = new Set(['PropsWithChildren', 'SFC', 'StatelessComponent', 'FunctionComponent', 'FC']); const genericReactTypesImport = new Set(); /** @@ -496,6 +496,36 @@ module.exports = function propTypesInstructions(context, components, utils) { return {}; } + function isValidReactGenericTypeAnnotation(annotation) { + if (annotation.typeName) { + if (annotation.typeName.name) { // if FC + const typeName = annotation.typeName.name; + if (!genericReactTypesImport.has(typeName)) { + return false; + } + } else if (annotation.typeName.right.name) { // if React.FC + const right = annotation.typeName.right.name; + const left = annotation.typeName.left.name; + + if (!genericReactTypesImport.has(left) || !allowedGenericTypes.has(right)) { + return false; + } + } + } + return true; + } + + /** + * Returns the left most typeName of a node, e.g: FC, React.FC + * The representation is used to verify nested used properties. + * @param {ASTNode} node + * @return {string | undefined} + */ + function getTypeName(node) { + if (node.name) return node.name; + if (node.left) return getTypeName(node.left); + } + class DeclarePropTypesForTSTypeAnnotation { constructor(propTypes, declaredPropTypes) { this.propTypes = propTypes; @@ -549,8 +579,13 @@ module.exports = function propTypesInstructions(context, components, utils) { let typeName; if (astUtil.isTSTypeReference(node)) { typeName = node.typeName.name; - const shouldTraverseTypeParams = !typeName || genericReactTypesImport.has(typeName); + const shouldTraverseTypeParams = genericReactTypesImport.has(getTypeName(node.typeName)); if (shouldTraverseTypeParams && node.typeParameters && node.typeParameters.length !== 0) { + // All react Generic types are derived from: + // type PropsWithChildren

= P & { children?: ReactNode | undefined } + // So we should construct an optional children prop + this.shouldSpecifyOptionalChildrenProps = true; + const nextNode = node.typeParameters.params[0]; this.visitTSNode(nextNode); return; @@ -725,6 +760,14 @@ module.exports = function propTypesInstructions(context, components, utils) { } endAndStructDeclaredPropTypes() { + if (this.shouldSpecifyOptionalChildrenProps) { + this.declaredPropTypes.children = { + fullName: 'children', + name: 'children', + node: {}, + isRequired: false + }; + } this.foundDeclaredPropertiesList.forEach((tsInterfaceBody) => { if (tsInterfaceBody && (tsInterfaceBody.type === 'TSPropertySignature' || tsInterfaceBody.type === 'TSMethodSignature')) { let accessor = 'name'; @@ -928,6 +971,16 @@ module.exports = function propTypesInstructions(context, components, utils) { } }); } else { + // check if its a valid generic type when `X<{...}>` + if ( + param.typeAnnotation + && param.typeAnnotation.typeAnnotation + && param.typeAnnotation.typeAnnotation.type === 'TSTypeReference' + && param.typeAnnotation.typeAnnotation.typeParameters != null + && !isValidReactGenericTypeAnnotation(param.typeAnnotation.typeAnnotation) + ) { + return; + } markPropTypesAsDeclared(node, resolveTypeAnnotation(param)); } } else { @@ -942,21 +995,8 @@ module.exports = function propTypesInstructions(context, components, utils) { return; } - if (annotation.typeName) { - if (annotation.typeName.name) { // if FC - const typeName = annotation.typeName.name; - if (!genericReactTypesImport.has(typeName)) { - return; - } - } else if (annotation.typeName.right.name) { // if React.FC - const right = annotation.typeName.right.name; - const left = annotation.typeName.left.name; + if (!isValidReactGenericTypeAnnotation(annotation)) return; - if (!genericReactTypesImport.has(left) || !allowedGenericTypes.has(right)) { - return; - } - } - } markPropTypesAsDeclared(node, resolveTypeAnnotation(siblingIdentifier)); } } diff --git a/tests/lib/rules/prop-types.js b/tests/lib/rules/prop-types.js index 1e778596ec..d02c5f94a1 100644 --- a/tests/lib/rules/prop-types.js +++ b/tests/lib/rules/prop-types.js @@ -3292,6 +3292,17 @@ ruleTester.run('prop-types', rule, { } `, parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + import React from 'react'; + + const MyComponent = (props: React.PropsWithChildren<{ username: string }>): React.ReactElement => { + return <>{props.children}{props.username}; + }; + + `, + parser: parsers['@TYPESCRIPT_ESLINT'] } ]), {