Skip to content

Commit f066443

Browse files
committed
Add support for defaultProps typing
1 parent f1becec commit f066443

File tree

8 files changed

+111
-24
lines changed

8 files changed

+111
-24
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
- Added
44
- Add support for more type annotations on methods
5+
- Add support for modifying types reflecting `defaultProps`
56

67
## 0.1.5
78

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ After:
7777
import React from "react";
7878

7979
type Props = {
80-
by: number;
80+
by?: number | undefined
8181
};
8282

8383
type State = {
@@ -160,7 +160,7 @@ export const C = props => {
160160
- [x] Transform `S` type argument
161161
- [x] Transform ref types
162162
- [ ] Transform generic components
163-
- [ ] Modify Props appropriately if defaultProps is present
163+
- [x] Modify Props appropriately if defaultProps is present
164164
- [ ] Modify Props appropriately if `children` seems to be used
165165
- [ ] Support for `this.props`
166166
- [x] Convert `this.props` to `props` parameter

img/example1-after.png

3.41 KB
Loading

src/analysis.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ export function analyzeBody(
7878
if (!renderPath) {
7979
throw new AnalysisError(`Missing render method`);
8080
}
81-
const props = analyzeProps(propsObjSites, defaultPropsObjSites, locals);
81+
const props = analyzeProps(propsObjSites, defaultPropsObjSites, locals, head);
8282
for (const [name, propAnalysis] of props.props) {
8383
if (propAnalysis.needsAlias) {
8484
propAnalysis.newAliasName = locals.newLocal(

src/analysis/head.ts

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { analyzeLibRef, isReactRef, LibRef } from "./lib.js";
66
export type ComponentHead = {
77
superClassRef: LibRef;
88
props: NodePath<TSType> | undefined;
9+
propsEach: Map<string, NodePath<TSPropertySignature | TSMethodSignature>>;
910
states: Map<string, NodePath<TSPropertySignature | TSMethodSignature>>;
1011
};
1112

@@ -35,36 +36,46 @@ export function analyzeHead(path: NodePath<ClassDeclaration>): ComponentHead | u
3536
}
3637
if (superClassRef.name === "Component" || superClassRef.name === "PureComponent") {
3738
let props: NodePath<TSType> | undefined;
38-
const states = new Map<string, NodePath<TSPropertySignature | TSMethodSignature>>();
39+
let propsEach: Map<string, NodePath<TSPropertySignature | TSMethodSignature>> | undefined = undefined;
40+
let states: Map<string, NodePath<TSPropertySignature | TSMethodSignature>> | undefined = undefined;
3941
const superTypeParameters = path.get("superTypeParameters");
4042
if (superTypeParameters.isTSTypeParameterInstantiation()) {
4143
const params = superTypeParameters.get("params");
4244
if (params.length > 0) {
4345
props = params[0];
46+
propsEach = decompose(params[0]!);
4447
}
4548
if (params.length > 1) {
4649
const stateParamPath = params[1]!;
47-
const statePath = resolveAlias(stateParamPath);
48-
const members =
49-
statePath.isTSTypeLiteral()
50-
? statePath.get("members")
51-
: statePath.isTSInterfaceBody()
52-
? statePath.get("body")
53-
: undefined;
54-
if (members) {
55-
for (const member of members) {
56-
if (member.isTSPropertySignature() || member.isTSMethodSignature()) {
57-
const name = memberName(member.node);
58-
if (name != null) {
59-
states.set(name, member);
60-
}
61-
}
62-
}
50+
states = decompose(stateParamPath);
51+
}
52+
}
53+
propsEach ??= new Map();
54+
states ??= new Map();
55+
return { superClassRef, props, propsEach, states };
56+
}
57+
}
58+
59+
function decompose(path: NodePath<TSType>): Map<string, NodePath<TSPropertySignature | TSMethodSignature>> {
60+
const aliasPath = resolveAlias(path);
61+
const members =
62+
aliasPath.isTSTypeLiteral()
63+
? aliasPath.get("members")
64+
: aliasPath.isTSInterfaceBody()
65+
? aliasPath.get("body")
66+
: undefined;
67+
const decomposed = new Map<string, NodePath<TSPropertySignature | TSMethodSignature>>();
68+
if (members) {
69+
for (const member of members) {
70+
if (member.isTSPropertySignature() || member.isTSMethodSignature()) {
71+
const name = memberName(member.node);
72+
if (name != null) {
73+
decomposed.set(name, member);
6374
}
6475
}
6576
}
66-
return { superClassRef, props, states };
6777
}
78+
return decomposed;
6879
}
6980

7081
function resolveAlias(path: NodePath<TSType>): NodePath<TSType | TSInterfaceBody> {

src/analysis/prop.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import type { NodePath } from "@babel/core";
22
import type { Scope } from "@babel/traverse";
3-
import type { Expression, MemberExpression } from "@babel/types";
3+
import type { Expression, MemberExpression, TSMethodSignature, TSPropertySignature } from "@babel/types";
44
import { getOr, memberName } from "../utils.js";
55
import { AnalysisError } from "./error.js";
66
import type { LocalManager } from "./local.js";
77
import { StaticFieldSite, ThisFieldSite } from "./this_fields.js";
88
import { trackMember } from "./track_member.js";
9+
import { ComponentHead } from "./head.js";
910

1011
export type PropsObjAnalysis = {
1112
hasDefaults: boolean;
@@ -23,6 +24,7 @@ export type PropAnalysis = {
2324
sites: PropSite[];
2425
aliases: PropAlias[];
2526
needsAlias: boolean;
27+
typing?: NodePath<TSPropertySignature | TSMethodSignature> | undefined;
2628
};
2729

2830
export type PropsObjSite = {
@@ -56,6 +58,7 @@ export function analyzeProps(
5658
propsObjSites: ThisFieldSite[],
5759
defaultPropsObjSites: StaticFieldSite[],
5860
locals: LocalManager,
61+
head: ComponentHead,
5962
): PropsObjAnalysis {
6063
const defaultProps = analyzeDefaultProps(defaultPropsObjSites);
6164
const newObjSites: PropsObjSite[] = [];
@@ -90,6 +93,9 @@ export function analyzeProps(
9093
}
9194
}
9295
}
96+
for (const [name, propTyping] of head.propsEach) {
97+
getProp(name).typing = propTyping;
98+
}
9399
if (defaultProps) {
94100
for (const [name, defaultValue] of defaultProps) {
95101
getProp(name).defaultValue = defaultValue;

src/index.test.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,46 @@ describe("react-declassify", () => {
615615
expect(transform(input)).toBe(output);
616616
});
617617

618+
it("Transforms types for defaultProps", () => {
619+
const input = dedent`\
620+
type Props = {
621+
foo: number;
622+
bar: number;
623+
baz: number;
624+
quux: number;
625+
};
626+
class C extends React.Component<Props> {
627+
static defaultProps = {
628+
foo: 42,
629+
quux: 0,
630+
};
631+
render() {
632+
const { foo, bar } = this.props;
633+
return foo + bar + this.props.baz + this.props.quux;
634+
}
635+
}
636+
`;
637+
const output = dedent`\
638+
type Props = {
639+
foo?: number | undefined
640+
bar: number;
641+
baz: number;
642+
quux?: number | undefined
643+
};
644+
645+
const C: React.FC<Props> = props => {
646+
const {
647+
foo = 42,
648+
bar,
649+
baz,
650+
quux = 0
651+
} = props;
652+
return foo + bar + baz + quux;
653+
};
654+
`;
655+
expect(transform(input, { ts: true })).toBe(output);
656+
});
657+
618658
it("transforms method types", () => {
619659
const input = dedent`\
620660
class C extends React.Component {
@@ -1025,7 +1065,7 @@ describe("react-declassify", () => {
10251065
import React from "react";
10261066
10271067
type Props = {
1028-
by: number;
1068+
by?: number | undefined
10291069
};
10301070
10311071
type State = {

src/index.ts

Lines changed: 30 additions & 1 deletion
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 } from "./utils.js";
3+
import { assignReturnType, assignTypeAnnotation, assignTypeParameters, importName, isTS, nonNullPath } from "./utils.js";
44
import { AnalysisError, analyzeBody, analyzeHead, ComponentBody, ComponentHead, needsProps, LibRef } from "./analysis.js";
55

66
type Options = {};
@@ -116,6 +116,35 @@ function transformClass(head: ComponentHead, body: ComponentBody, options: { ts:
116116
site.path.replaceWith(site.path.node.property);
117117
}
118118
}
119+
for (const [, prop] of body.props.props) {
120+
if (prop.defaultValue && prop.typing) {
121+
// Make the prop optional
122+
prop.typing.node.optional = true;
123+
if (prop.typing.isTSPropertySignature()) {
124+
const typeAnnotation = nonNullPath(prop.typing.get("typeAnnotation"))?.get("typeAnnotation");
125+
if (typeAnnotation) {
126+
if (typeAnnotation.isTSUnionType()) {
127+
if (typeAnnotation.node.types.some((t) => t.type === "TSUndefinedKeyword")) {
128+
// No need to add undefined
129+
} else {
130+
typeAnnotation.node.types.push(t.tsUndefinedKeyword());
131+
}
132+
} else {
133+
typeAnnotation.replaceWith(t.tsUnionType([
134+
typeAnnotation.node,
135+
t.tsUndefinedKeyword(),
136+
]))
137+
}
138+
}
139+
}
140+
if (
141+
prop.typing.node.type === "TSPropertySignature"
142+
&& prop.typing.node.typeAnnotation
143+
) {
144+
const typeAnnot = prop.typing.node.typeAnnotation
145+
}
146+
}
147+
}
119148
for (const [name, stateAnalysis] of body.state) {
120149
for (const site of stateAnalysis.sites) {
121150
if (site.type === "expr") {

0 commit comments

Comments
 (0)