Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const MyComponent = function Component(
ref: myRef,
...myProps
}: { a: 1 } & {
ref: React.RefObject<unknown>
ref?: React.Ref<unknown>
}
) {
return null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { forwardRef } from 'react';
import type { ForwardedRef } from 'react';

const MyComponent = forwardRef(function Component(
myProps: Props,
myRef: ForwardedRef<HTMLDivElement>
) {
return null;
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { ForwardedRef } from 'react';

const MyComponent = function Component(
{
ref: myRef,
...myProps
}: Props & {
ref?: React.Ref<HTMLDivElement>
}
) {
return null;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { forwardRef } from 'react';

const MyTab = forwardRef(function Tab(
props: TabProps | LinkTabProps,
ref: React.ForwardedRef<HTMLButtonElement | HTMLAnchorElement>
) {
return null;
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const MyTab = function Tab(
{
ref,
...props
}: (TabProps | LinkTabProps) & {
ref?: React.Ref<HTMLButtonElement | HTMLAnchorElement>
}
) {
return null;
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const MyComponent = function Component(
ref: myRef,
...myProps
}: Props & {
ref: React.RefObject<HTMLButtonElement>
ref?: React.Ref<HTMLButtonElement>
}
) {
return null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { forwardRef } from 'react';

const MyTab = forwardRef<HTMLButtonElement, PropsWithChildren<Props> & ExtraProps>((props, ref) => {
return null;
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const MyTab = (
{
ref,
...props
}: PropsWithChildren<Props> & ExtraProps & {
ref?: React.Ref<HTMLButtonElement>
}
) => {
return null;
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const MyInput = (
ref,
...props
}: { a: string } & {
ref: React.RefObject<RefValueType>
ref?: React.Ref<RefValueType>
}
) => {
return null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { forwardRef } from 'react';

const MyElement = forwardRef<HTMLDivElement, (BaseProps & MetaProps) | (ButtonProps & LinkProps)>((props, ref) => {
return null;
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const MyElement = (
{
ref,
...props
}: ((BaseProps & MetaProps) | (ButtonProps & LinkProps)) & {
ref?: React.Ref<HTMLDivElement>
}
) => {
return null;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { forwardRef } from 'react';

const MyInput = forwardRef<HTMLInputElement, TextProps | NumberProps>((props, ref) => {
return null;
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const MyInput = (
{
ref,
...props
}: (TextProps | NumberProps) & {
ref?: React.Ref<HTMLInputElement>
}
) => {
return null;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { forwardRef } from 'react';

const MyButton = forwardRef<HTMLButtonElement | HTMLAnchorElement, Props>((props, ref) => {
return null;
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const MyButton = (
{
ref,
...props
}: Props & {
ref?: React.Ref<HTMLButtonElement | HTMLAnchorElement>
}
) => {
return null;
};
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const MyInput = (
ref,
...props
}: Props & {
ref: React.RefObject<HTMLInputElement>
ref?: React.Ref<HTMLInputElement>
}
) => {
return null;
Expand Down
14 changes: 10 additions & 4 deletions transforms/__tests__/remove-forward-ref.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
65 changes: 40 additions & 25 deletions transforms/remove-forward-ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,27 +6,34 @@ import type {
FunctionExpression,
Identifier,
JSCodeshift,
TSTypeLiteral,
TSTypeReference,
TSType,
} from 'jscodeshift';

// Props & { ref: React.RefObject<Ref>}
// 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<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,
Expand All @@ -53,25 +60,39 @@ const buildRefAndPropsObjectPattern = (
j.restProperty(j.identifier(propArgName)),
]);

const REF_WRAPPER_TYPE_NAMES = [
'ForwardedRef',
'Ref',
'LegacyRef',
'MutableRefObject',
'RefObject',
];

// React.ForwardedRef<HTMLButtonElement> => HTMLButtonElement
// ForwardedRef<HTMLButtonElement> => 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;
}

Expand All @@ -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) {
Expand Down Expand Up @@ -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]
) {
Expand All @@ -240,18 +258,15 @@ 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<Ref, Props>((props) => { ... }) ====> (props: Props & { ref: React.RefObject<Ref> }) => { ... }
// forwardRef<Ref, Props>((props) => { ... }) ====> (props: Props & { ref?: React.Ref<Ref> }) => { ... }
if (
j.TSTypeParameterInstantiation.check(typeParameters) &&
renderFunction.params?.[0] &&
'typeAnnotation' in renderFunction.params[0]
) {
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);

Expand Down