diff --git a/CHANGELOG.md b/CHANGELOG.md index f4e364fc20..3d8f285d46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,8 +14,10 @@ This change log adheres to standards from [Keep a CHANGELOG](http://keepachangel * [`forbid-component-props`]: Implemented support for "namespaced" components ([#2767][] @mnn) * [`prefer-read-only-props`]: support Flow `$ReadOnly` ([#2772][], [#2779][], [#2770][] @karolina-benitez) * [`jsx-handler-names`]: handle whitespace ([#2789][] @AriPerkkio) +* [`prop-types`]: Detect TypeScript types for destructured default prop values ([#2780][] @sunghyunjo) [#2789]: https://github.com/yannickcr/eslint-plugin-react/pull/2789 +[#2780]: https://github.com/yannickcr/eslint-plugin-react/pull/2780 [#2779]: https://github.com/yannickcr/eslint-plugin-react/pull/2779 [#2772]: https://github.com/yannickcr/eslint-plugin-react/pull/2772 [#2771]: https://github.com/yannickcr/eslint-plugin-react/pull/2771 diff --git a/docs/rules/prop-types.md b/docs/rules/prop-types.md index b0e65baddf..0a70baf9ba 100644 --- a/docs/rules/prop-types.md +++ b/docs/rules/prop-types.md @@ -91,6 +91,20 @@ class HelloEs6WithPublicClassField extends React.Component { } ``` +In TypeScript: + +```tsx +// destructured default prop values + +function Foo({ bar = "" }): JSX.Element { + return
{bar}
; +} + +function Foo({ bar = "" as string }): JSX.Element { + return
{bar}
; +} +``` + In Flow: ```tsx diff --git a/lib/rules/prop-types.js b/lib/rules/prop-types.js index b1538193fb..ee939df065 100644 --- a/lib/rules/prop-types.js +++ b/lib/rules/prop-types.js @@ -137,6 +137,34 @@ module.exports = { return true; } + /** + * Checks if the prop is declared in destructured params + * @param {Object[]} params List of destructured param among props without declaredPropTypes + * @returns {Boolean} True if the prop is declared, false if not. + */ + function isDeclaredInDestructuredParam(params) { + let result = true; + params.forEach((param) => { + if (!param.properties) { + result = false; + return; + } + param.properties.forEach((property) => { + const type = property.value.type; + const right = property.value.right; + if (type !== 'AssignmentPattern') { + result = false; + return; + } + if (type === 'AssignmentPattern' && right && right.expression && right.expression.type && right.expression.type !== 'Literal') { + result = false; + } + }); + }); + + return result; + } + /** * Checks if the prop is declared * @param {ASTNode} node The AST node being checked. @@ -149,9 +177,14 @@ module.exports = { const isDeclared = component && component.confidence === 2 && internalIsDeclaredInComponent(component.declaredPropTypes || {}, names); + if (isDeclared) { return true; } + + if (component && !isDeclared && !component.declaredPropTypes && component.node.params && (component.node.type === 'FunctionDeclaration' || component.node.type === 'FunctionExpression' || component.node.type === 'ArrowFunctionExpression')) { + return isDeclaredInDestructuredParam(component.node.params); + } node = node.parent; } return false; diff --git a/tests/lib/rules/prop-types.js b/tests/lib/rules/prop-types.js index 82a6a60079..29b04794cd 100644 --- a/tests/lib/rules/prop-types.js +++ b/tests/lib/rules/prop-types.js @@ -34,7 +34,6 @@ const settings = { const ruleTester = new RuleTester({parserOptions}); ruleTester.run('prop-types', rule, { - valid: [].concat( { code: [ @@ -2965,6 +2964,54 @@ ruleTester.run('prop-types', rule, { }; `, parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + function Foo({ bar = "" }: { bar: string }): JSX.Element { + return
{bar}
; + } + `, + parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + function Foo({ foo = "" }): JSX.Element { + return
{foo}
; + } + `, + parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + function Foo({ bar = "" as string }): JSX.Element { + return
{bar}
; + } + `, + parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + export default function ({ value = 'World' }) { + return

Hello {value}

+ } + `, + parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + const Foo: JSX.Element = ({ bar = "" }) => { + return
{bar}
; + } + `, + parser: parsers['@TYPESCRIPT_ESLINT'] + }, + { + code: ` + const Foo: JSX.Element = function foo ({ bar = "" }) { + return
{bar}
; + } + `, + parser: parsers['@TYPESCRIPT_ESLINT'] } ]) ), @@ -5544,16 +5591,6 @@ ruleTester.run('prop-types', rule, { message: '\'foo.baz\' is missing in props validation' }] }, - { - code: ` - export default function ({ value = 'World' }) { - return

Hello {value}

- } - `, - errors: [{ - message: '\'value\' is missing in props validation' - }] - }, parsers.TS([ { code: ` @@ -5958,6 +5995,30 @@ ruleTester.run('prop-types', rule, { errors: [{ message: "'foo.c' is missing in props validation" }] + }, + { + code: ` + const Foo: JSX.Element = ({ bar }) => { + return
{bar}
; + } + `, + settings, + parser: parsers['@TYPESCRIPT_ESLINT'], + errors: [{ + message: "'bar' is missing in props validation" + }] + }, + { + code: ` + const Foo: JSX.Element = function foo ({ bar }) { + return
{bar}
; + } + `, + settings, + parser: parsers['@TYPESCRIPT_ESLINT'], + errors: [{ + message: "'bar' is missing in props validation" + }] } ]) )