Skip to content

Commit 3b299b4

Browse files
committed
Transform type-generic components
1 parent 26d8564 commit 3b299b4

File tree

6 files changed

+97
-26
lines changed

6 files changed

+97
-26
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
- Add support for more type annotations on methods
55
- Add support for modifying types reflecting `defaultProps`
66
- Add support for `React.PureComponent`
7+
- Add support for generics
78

89
## 0.1.5
910

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ export const C = props => {
159159
- [x] Transform `P` type argument
160160
- [x] Transform `S` type argument
161161
- [x] Transform ref types
162-
- [ ] Transform generic components
162+
- [x] Transform generic components
163163
- [x] Modify Props appropriately if defaultProps is present
164164
- [ ] Modify Props appropriately if `children` seems to be used
165165
- [ ] Support for `this.props`

src/analysis/head.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import type { NodePath } from "@babel/core";
2-
import type { BlockStatement, ClassDeclaration, Identifier, Program, TSInterfaceBody, TSMethodSignature, TSPropertySignature, TSType } from "@babel/types";
3-
import { memberName } from "../utils.js";
2+
import type { BlockStatement, ClassDeclaration, Identifier, Program, TSInterfaceBody, TSMethodSignature, TSPropertySignature, TSType, TSTypeParameterDeclaration } from "@babel/types";
3+
import { memberName, nonNullPath } from "../utils.js";
44
import { analyzeLibRef, isReactRef, LibRef } from "./lib.js";
55

66
export type ComponentHead = {
77
name?: Identifier | undefined;
8+
typeParameters?: NodePath<TSTypeParameterDeclaration> | undefined;
89
superClassRef: LibRef;
910
isPure: boolean;
1011
props: NodePath<TSType> | undefined;
@@ -38,6 +39,8 @@ export function analyzeHead(path: NodePath<ClassDeclaration>): ComponentHead | u
3839
}
3940
if (superClassRef.name === "Component" || superClassRef.name === "PureComponent") {
4041
const name = path.node.id;
42+
const typeParameters_ = nonNullPath(path.get("typeParameters"));
43+
const typeParameters = typeParameters_?.isTSTypeParameterDeclaration() ? typeParameters_ : undefined;
4144
const isPure = superClassRef.name === "PureComponent";
4245
let props: NodePath<TSType> | undefined;
4346
let propsEach: Map<string, NodePath<TSPropertySignature | TSMethodSignature>> | undefined = undefined;
@@ -56,7 +59,7 @@ export function analyzeHead(path: NodePath<ClassDeclaration>): ComponentHead | u
5659
}
5760
propsEach ??= new Map();
5861
states ??= new Map();
59-
return { name, superClassRef, isPure, props, propsEach, states };
62+
return { name, typeParameters, superClassRef, isPure, props, propsEach, states };
6063
}
6164
}
6265

src/index.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,29 @@ describe("react-declassify", () => {
9494
`;
9595
expect(transform(input, { ts: true })).toBe(output);
9696
});
97+
98+
it("transforms type parameters", () => {
99+
const input = dedent`\
100+
type Props<T> = {
101+
text: T;
102+
};
103+
class C<T> extends React.Component<Props<T>> {
104+
render() {
105+
return <div>Hello, {this.props.text}!</div>;
106+
}
107+
}
108+
`;
109+
const output = dedent`\
110+
type Props<T> = {
111+
text: T;
112+
};
113+
114+
const C = function C<T>(props: Props<T>): React.ReactElement | null {
115+
return <div>Hello, {props.text}!</div>;
116+
};
117+
`;
118+
expect(transform(input, { ts: true })).toBe(output);
119+
});
97120
});
98121

99122
it("doesn't transform empty Component class", () => {

src/index.ts

Lines changed: 52 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { ArrowFunctionExpression, ClassMethod, ClassPrivateMethod, Expression, FunctionDeclaration, FunctionExpression, Identifier, ImportDeclaration, MemberExpression, ObjectMethod, Pattern, RestElement, Statement, TSEntityName, TSType, TSTypeAnnotation } from "@babel/types";
22
import type { NodePath, PluginObj, PluginPass } from "@babel/core";
3-
import { assignReturnType, assignTypeAnnotation, assignTypeParameters, importName, isTS, nonNullPath } from "./utils.js";
3+
import { assignReturnType, assignTypeAnnotation, assignTypeArguments, assignTypeParameters, importName, isTS, nonNullPath } from "./utils.js";
44
import { AnalysisError, analyzeBody, analyzeHead, ComponentBody, ComponentHead, needsProps, LibRef } from "./analysis.js";
55

66
type Options = {};
@@ -25,10 +25,10 @@ export default function plugin(babel: typeof import("@babel/core")): PluginObj<P
2525
declPath.replaceWithMultiple([
2626
t.variableDeclaration("const", [
2727
t.variableDeclarator(
28-
ts
28+
typeNode
2929
? assignTypeAnnotation(
3030
t.cloneNode(path.node.id),
31-
t.tsTypeAnnotation(typeNode!),
31+
t.tsTypeAnnotation(typeNode),
3232
)
3333
: t.cloneNode(path.node.id),
3434
funcNode,
@@ -54,10 +54,10 @@ export default function plugin(babel: typeof import("@babel/core")): PluginObj<P
5454
const { funcNode, typeNode } = transformClass(head, body, { ts }, babel);
5555
path.replaceWith(t.variableDeclaration("const", [
5656
t.variableDeclarator(
57-
ts
57+
typeNode
5858
? assignTypeAnnotation(
5959
t.cloneNode(path.node.id),
60-
t.tsTypeAnnotation(typeNode!),
60+
t.tsTypeAnnotation(typeNode),
6161
)
6262
: t.cloneNode(path.node.id),
6363
funcNode,
@@ -221,7 +221,7 @@ function transformClass(head: ComponentHead, body: ComponentBody, options: { ts:
221221
t.identifier(field.localSetterName!),
222222
]),
223223
ts && field.typeAnnotation ?
224-
assignTypeParameters(
224+
assignTypeArguments(
225225
call,
226226
t.tsTypeParameterInstantiation([
227227
field.typeAnnotation.type === "method"
@@ -278,7 +278,7 @@ function transformClass(head: ComponentHead, body: ComponentBody, options: { ts:
278278
[t.variableDeclarator(
279279
t.identifier(field.localName!),
280280
ts && field.typeAnnotation
281-
? assignTypeParameters(
281+
? assignTypeArguments(
282282
call,
283283
t.tsTypeParameterInstantiation([
284284
field.typeAnnotation.node
@@ -298,7 +298,7 @@ function transformClass(head: ComponentHead, body: ComponentBody, options: { ts:
298298
[t.variableDeclarator(
299299
t.identifier(field.localName!),
300300
ts && field.typeAnnotation
301-
? assignTypeParameters(
301+
? assignTypeArguments(
302302
call,
303303
t.tsTypeParameterInstantiation([
304304
field.typeAnnotation.node
@@ -311,25 +311,57 @@ function transformClass(head: ComponentHead, body: ComponentBody, options: { ts:
311311
}
312312
const bodyNode = body.render.path.node.body;
313313
bodyNode.body.splice(0, 0, ...preamble);
314-
const functionNeeded = head.isPure;
315-
const funcNode = functionNeeded
316-
? t.functionExpression(
317-
head.name ? t.cloneNode(head.name) : undefined,
318-
needsProps(body) ? [t.identifier("props")] : [],
319-
bodyNode
320-
)
321-
: t.arrowFunctionExpression(
322-
needsProps(body) ? [t.identifier("props")] : [],
323-
bodyNode
324-
);
314+
// recast is not smart enough to correctly pretty-print type parameters for arrow functions.
315+
// so we fall back to functions when type parameters are present.
316+
const functionNeeded = head.isPure || !!head.typeParameters;
317+
const params = needsProps(body)
318+
? [assignTypeAnnotation(
319+
t.identifier("props"),
320+
// If the function is generic, put type annotations here instead of the `const` to be defined.
321+
// TODO: take children into account, while being careful about difference between `@types/react` v17 and v18
322+
head.typeParameters
323+
? head.props
324+
? t.tsTypeAnnotation(head.props.node)
325+
: undefined
326+
: undefined
327+
)]
328+
: [];
329+
// If the function is generic, put type annotations here instead of the `const` to be defined.
330+
const returnType = head.typeParameters
331+
// Construct `React.ReactElement | null`
332+
? t.tsTypeAnnotation(
333+
t.tsUnionType([
334+
t.tsTypeReference(
335+
toTSEntity(getReactImport("ReactElement", babel, head.superClassRef), babel)
336+
),
337+
t.tsNullKeyword(),
338+
])
339+
)
340+
: undefined;
341+
const funcNode = assignTypeParameters(
342+
assignReturnType(
343+
functionNeeded
344+
? t.functionExpression(
345+
head.name ? t.cloneNode(head.name) : undefined,
346+
params,
347+
bodyNode
348+
)
349+
: t.arrowFunctionExpression(
350+
params,
351+
bodyNode
352+
),
353+
returnType
354+
),
355+
head.typeParameters?.node
356+
);
325357
return {
326358
funcNode: head.isPure
327359
? t.callExpression(
328360
getReactImport("memo", babel, head.superClassRef),
329361
[funcNode]
330362
)
331363
: funcNode,
332-
typeNode: ts
364+
typeNode: ts && !head.typeParameters
333365
? t.tsTypeReference(
334366
toTSEntity(getReactImport("FC", babel, head.superClassRef), babel),
335367
head.props

src/utils.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { NodePath, PluginPass } from "@babel/core";
2-
import type { ArrayPattern, ArrowFunctionExpression, AssignmentPattern, CallExpression, ClassAccessorProperty, ClassMethod, ClassPrivateMethod, ClassPrivateProperty, ClassProperty, FunctionDeclaration, FunctionExpression, Identifier, JSXOpeningElement, MemberExpression, NewExpression, Noop, ObjectMethod, ObjectPattern, ObjectProperty, OptionalCallExpression, RestElement, StaticBlock, StringLiteral, TaggedTemplateExpression, TSDeclareFunction, TSDeclareMethod, TSExpressionWithTypeArguments, TSImportType, TSInstantiationExpression, TSMethodSignature, TSPropertySignature, TSTypeAnnotation, TSTypeParameterInstantiation, TSTypeQuery, TSTypeReference, TypeAnnotation } from "@babel/types";
2+
import type { ArrayPattern, ArrowFunctionExpression, AssignmentPattern, CallExpression, ClassAccessorProperty, ClassDeclaration, ClassExpression, ClassMethod, ClassPrivateMethod, ClassPrivateProperty, ClassProperty, FunctionDeclaration, FunctionExpression, Identifier, JSXOpeningElement, MemberExpression, NewExpression, Noop, ObjectMethod, ObjectPattern, ObjectProperty, OptionalCallExpression, RestElement, StaticBlock, StringLiteral, TaggedTemplateExpression, TSCallSignatureDeclaration, TSConstructorType, TSConstructSignatureDeclaration, TSDeclareFunction, TSDeclareMethod, TSExpressionWithTypeArguments, TSFunctionType, TSImportType, TSInstantiationExpression, TSInterfaceDeclaration, TSMethodSignature, TSPropertySignature, TSTypeAliasDeclaration, TSTypeAnnotation, TSTypeParameterDeclaration, TSTypeParameterInstantiation, TSTypeQuery, TSTypeReference, TypeAnnotation } from "@babel/types";
33

44
export function getOr<K, V>(m: Map<K, V>, k: K, getDefault: () => V): V {
55
if (m.has(k)) {
@@ -109,9 +109,21 @@ export function assignReturnType<T extends ReturnTypeable>(
109109
}
110110

111111
type Paramable =
112-
CallExpression | NewExpression | TaggedTemplateExpression | OptionalCallExpression | JSXOpeningElement | TSTypeReference | TSTypeQuery | TSExpressionWithTypeArguments | TSInstantiationExpression | TSImportType;
112+
FunctionDeclaration | FunctionExpression | ArrowFunctionExpression | TSDeclareFunction | ObjectMethod | ClassMethod | ClassPrivateMethod | TSDeclareMethod | ClassDeclaration | ClassExpression | TSCallSignatureDeclaration | TSConstructSignatureDeclaration | TSMethodSignature | TSFunctionType | TSConstructorType | TSInterfaceDeclaration | TSTypeAliasDeclaration;
113113

114114
export function assignTypeParameters<T extends Paramable>(
115+
node: T,
116+
typeParameters: TSTypeParameterDeclaration | null | undefined,
117+
): T {
118+
return Object.assign(node, {
119+
typeParameters,
120+
});
121+
}
122+
123+
type Arguable =
124+
CallExpression | NewExpression | TaggedTemplateExpression | OptionalCallExpression | JSXOpeningElement | TSTypeReference | TSTypeQuery | TSExpressionWithTypeArguments | TSInstantiationExpression | TSImportType;
125+
126+
export function assignTypeArguments<T extends Arguable>(
115127
node: T,
116128
typeParameters: TSTypeParameterInstantiation | null | undefined,
117129
): T {

0 commit comments

Comments
 (0)