diff --git a/transforms/__testfixtures__/remove-forward-ref/typescript/props-type-literal.output.js b/transforms/__testfixtures__/remove-forward-ref/typescript/props-type-literal.output.js index 56fa6f5..b7bd3f6 100644 --- a/transforms/__testfixtures__/remove-forward-ref/typescript/props-type-literal.output.js +++ b/transforms/__testfixtures__/remove-forward-ref/typescript/props-type-literal.output.js @@ -3,7 +3,7 @@ const MyComponent = function Component( ref: myRef, ...myProps }: { a: 1 } & { - ref: React.RefObject + ref?: React.Ref } ) { return null; diff --git a/transforms/__testfixtures__/remove-forward-ref/typescript/ref-arg-named-import.input.js b/transforms/__testfixtures__/remove-forward-ref/typescript/ref-arg-named-import.input.js new file mode 100644 index 0000000..673e06e --- /dev/null +++ b/transforms/__testfixtures__/remove-forward-ref/typescript/ref-arg-named-import.input.js @@ -0,0 +1,9 @@ +import { forwardRef } from 'react'; +import type { ForwardedRef } from 'react'; + +const MyComponent = forwardRef(function Component( + myProps: Props, + myRef: ForwardedRef +) { + return null; +}); diff --git a/transforms/__testfixtures__/remove-forward-ref/typescript/ref-arg-named-import.output.js b/transforms/__testfixtures__/remove-forward-ref/typescript/ref-arg-named-import.output.js new file mode 100644 index 0000000..dadd064 --- /dev/null +++ b/transforms/__testfixtures__/remove-forward-ref/typescript/ref-arg-named-import.output.js @@ -0,0 +1,12 @@ +import type { ForwardedRef } from 'react'; + +const MyComponent = function Component( + { + ref: myRef, + ...myProps + }: Props & { + ref?: React.Ref + } +) { + return null; +}; diff --git a/transforms/__testfixtures__/remove-forward-ref/typescript/ref-arg-union-type.input.js b/transforms/__testfixtures__/remove-forward-ref/typescript/ref-arg-union-type.input.js new file mode 100644 index 0000000..908e004 --- /dev/null +++ b/transforms/__testfixtures__/remove-forward-ref/typescript/ref-arg-union-type.input.js @@ -0,0 +1,8 @@ +import { forwardRef } from 'react'; + +const MyTab = forwardRef(function Tab( + props: TabProps | LinkTabProps, + ref: React.ForwardedRef +) { + return null; +}); diff --git a/transforms/__testfixtures__/remove-forward-ref/typescript/ref-arg-union-type.output.js b/transforms/__testfixtures__/remove-forward-ref/typescript/ref-arg-union-type.output.js new file mode 100644 index 0000000..5997dfe --- /dev/null +++ b/transforms/__testfixtures__/remove-forward-ref/typescript/ref-arg-union-type.output.js @@ -0,0 +1,10 @@ +const MyTab = function Tab( + { + ref, + ...props + }: (TabProps | LinkTabProps) & { + ref?: React.Ref + } +) { + return null; +}; diff --git a/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments-custom-names.output.js b/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments-custom-names.output.js index e868776..28941be 100644 --- a/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments-custom-names.output.js +++ b/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments-custom-names.output.js @@ -3,7 +3,7 @@ const MyComponent = function Component( ref: myRef, ...myProps }: Props & { - ref: React.RefObject + ref?: React.Ref } ) { return null; diff --git a/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments-intersection-props.input.js b/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments-intersection-props.input.js new file mode 100644 index 0000000..3f11de9 --- /dev/null +++ b/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments-intersection-props.input.js @@ -0,0 +1,5 @@ +import { forwardRef } from 'react'; + +const MyTab = forwardRef & ExtraProps>((props, ref) => { + return null; +}); diff --git a/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments-intersection-props.output.js b/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments-intersection-props.output.js new file mode 100644 index 0000000..c567737 --- /dev/null +++ b/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments-intersection-props.output.js @@ -0,0 +1,10 @@ +const MyTab = ( + { + ref, + ...props + }: PropsWithChildren & ExtraProps & { + ref?: React.Ref + } +) => { + return null; +}; diff --git a/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments-type-literals.output.js b/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments-type-literals.output.js index f569dfc..35ee10b 100644 --- a/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments-type-literals.output.js +++ b/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments-type-literals.output.js @@ -3,7 +3,7 @@ const MyInput = ( ref, ...props }: { a: string } & { - ref: React.RefObject + ref?: React.Ref } ) => { return null; diff --git a/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments-union-of-intersections.input.js b/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments-union-of-intersections.input.js new file mode 100644 index 0000000..7713f35 --- /dev/null +++ b/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments-union-of-intersections.input.js @@ -0,0 +1,5 @@ +import { forwardRef } from 'react'; + +const MyElement = forwardRef((props, ref) => { + return null; +}); diff --git a/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments-union-of-intersections.output.js b/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments-union-of-intersections.output.js new file mode 100644 index 0000000..d6d8b86 --- /dev/null +++ b/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments-union-of-intersections.output.js @@ -0,0 +1,10 @@ +const MyElement = ( + { + ref, + ...props + }: ((BaseProps & MetaProps) | (ButtonProps & LinkProps)) & { + ref?: React.Ref + } +) => { + return null; +}; diff --git a/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments-union-props.input.js b/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments-union-props.input.js new file mode 100644 index 0000000..d7dcd8c --- /dev/null +++ b/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments-union-props.input.js @@ -0,0 +1,5 @@ +import { forwardRef } from 'react'; + +const MyInput = forwardRef((props, ref) => { + return null; +}); diff --git a/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments-union-props.output.js b/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments-union-props.output.js new file mode 100644 index 0000000..d4b4bc7 --- /dev/null +++ b/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments-union-props.output.js @@ -0,0 +1,10 @@ +const MyInput = ( + { + ref, + ...props + }: (TextProps | NumberProps) & { + ref?: React.Ref + } +) => { + return null; +}; diff --git a/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments-union-ref.input.js b/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments-union-ref.input.js new file mode 100644 index 0000000..51b35bb --- /dev/null +++ b/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments-union-ref.input.js @@ -0,0 +1,5 @@ +import { forwardRef } from 'react'; + +const MyButton = forwardRef((props, ref) => { + return null; +}); diff --git a/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments-union-ref.output.js b/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments-union-ref.output.js new file mode 100644 index 0000000..cc3c556 --- /dev/null +++ b/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments-union-ref.output.js @@ -0,0 +1,10 @@ +const MyButton = ( + { + ref, + ...props + }: Props & { + ref?: React.Ref + } +) => { + return null; +}; diff --git a/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments.output.js b/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments.output.js index 23e1c16..c0f172c 100644 --- a/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments.output.js +++ b/transforms/__testfixtures__/remove-forward-ref/typescript/type-arguments.output.js @@ -5,7 +5,7 @@ const MyInput = ( ref, ...props }: Props & { - ref: React.RefObject + ref?: React.Ref } ) => { return null; diff --git a/transforms/__tests__/remove-forward-ref.test.ts b/transforms/__tests__/remove-forward-ref.test.ts index 9f76f62..1c0cc89 100644 --- a/transforms/__tests__/remove-forward-ref.test.ts +++ b/transforms/__tests__/remove-forward-ref.test.ts @@ -12,10 +12,16 @@ const jsTests = [ ]; const tsTests = [ - 'type-arguments', - 'type-arguments-custom-names', - 'type-arguments-type-literals', - 'props-type-literal' + 'type-arguments', + 'type-arguments-custom-names', + 'type-arguments-type-literals', + 'type-arguments-union-ref', + 'type-arguments-union-props', + 'type-arguments-union-of-intersections', + 'type-arguments-intersection-props', + 'props-type-literal', + 'ref-arg-named-import', + 'ref-arg-union-type' ]; const defineTest = require('jscodeshift/dist/testUtils').defineTest; diff --git a/transforms/remove-forward-ref.ts b/transforms/remove-forward-ref.ts index da7ad9d..a7ed4a9 100644 --- a/transforms/remove-forward-ref.ts +++ b/transforms/remove-forward-ref.ts @@ -6,27 +6,34 @@ import type { FunctionExpression, Identifier, JSCodeshift, - TSTypeLiteral, - TSTypeReference, + TSType, } from 'jscodeshift'; -// Props & { ref: React.RefObject} +// union and conditional types bind looser than an intersection, so they +// need to be parenthesized when used as an intersection member +const parenthesizeForIntersection = (j: JSCodeshift, type: TSType) => + j.TSUnionType.check(type) || j.TSConditionalType.check(type) + ? j.tsParenthesizedType(type) + : type; + +// Props & { ref?: React.Ref } const buildPropsAndRefIntersectionTypeAnnotation = ( j: JSCodeshift, - propType: TSTypeReference | TSTypeLiteral, - refType: TSTypeReference | TSTypeLiteral | null, + propType: TSType, + refType: TSType | null, ) => j.tsTypeAnnotation( j.tsIntersectionType([ - propType, + parenthesizeForIntersection(j, propType), j.tsTypeLiteral([ j.tsPropertySignature.from({ key: j.identifier('ref'), + optional: true, typeAnnotation: j.tsTypeAnnotation( j.tsTypeReference.from({ typeName: j.tsQualifiedName( j.identifier('React'), - j.identifier('RefObject'), + j.identifier('Ref'), ), typeParameters: j.tsTypeParameterInstantiation([ refType === null ? j.tsUnknownKeyword() : refType, @@ -53,25 +60,39 @@ const buildRefAndPropsObjectPattern = ( j.restProperty(j.identifier(propArgName)), ]); +const REF_WRAPPER_TYPE_NAMES = [ + 'ForwardedRef', + 'Ref', + 'LegacyRef', + 'MutableRefObject', + 'RefObject', +]; + // React.ForwardedRef => HTMLButtonElement +// ForwardedRef => HTMLButtonElement const getRefTypeFromRefArg = (j: JSCodeshift, refArg: Identifier) => { const typeReference = refArg.typeAnnotation?.typeAnnotation; - if ( - !j.TSTypeReference.check(typeReference) || - !j.TSQualifiedName.check(typeReference.typeName) - ) { + if (!j.TSTypeReference.check(typeReference)) { return null; } - const { right } = typeReference.typeName; + const { typeName } = typeReference; - if (!j.Identifier.check(right) || right.name === 'forwardedRef') { + const wrapperName = j.TSQualifiedName.check(typeName) + ? j.Identifier.check(typeName.right) + ? typeName.right.name + : null + : j.Identifier.check(typeName) + ? typeName.name + : null; + + if (wrapperName === null || !REF_WRAPPER_TYPE_NAMES.includes(wrapperName)) { return null; } const [firstTypeParameter] = typeReference.typeParameters?.params ?? []; - if (!j.TSTypeReference.check(firstTypeParameter)) { + if (!j.TSType.check(firstTypeParameter)) { return null; } @@ -94,11 +115,8 @@ const getForwardRefRenderFunction = ( return renderFunction; }; -const isLiteralOrReference = ( - j: JSCodeshift, - type: unknown, -): type is TSTypeReference | TSTypeLiteral => { - return j.TSTypeReference.check(type) || j.TSTypeLiteral.check(type); +const isTSType = (j: JSCodeshift, type: unknown): type is TSType => { + return j.TSType.check(type); }; export default function transform(file: FileInfo, api: API) { @@ -220,7 +238,7 @@ export default function transform(file: FileInfo, api: API) { */ if ( - isLiteralOrReference(j, propsArgTypeReference) && + isTSType(j, propsArgTypeReference) && renderFunction.params?.[0] && 'typeAnnotation' in renderFunction.params[0] ) { @@ -240,7 +258,7 @@ export default function transform(file: FileInfo, api: API) { const typeParameters = callExpressionPath.node.typeParameters; // if typeParameters are used in forwardRef generic, reuse them to annotate props type - // forwardRef((props) => { ... }) ====> (props: Props & { ref: React.RefObject }) => { ... } + // forwardRef((props) => { ... }) ====> (props: Props & { ref?: React.Ref }) => { ... } if ( j.TSTypeParameterInstantiation.check(typeParameters) && renderFunction.params?.[0] && @@ -248,10 +266,7 @@ export default function transform(file: FileInfo, api: API) { ) { const [refType, propType] = typeParameters.params; - if ( - j.TSTypeReference.check(refType) && - isLiteralOrReference(j, propType) - ) { + if (isTSType(j, refType) && isTSType(j, propType)) { renderFunction.params[0].typeAnnotation = buildPropsAndRefIntersectionTypeAnnotation(j, propType, refType);