Skip to content

Commit 40ebb00

Browse files
authored
Support TypeScript builtin prop types (#862)
1 parent 74b6680 commit 40ebb00

8 files changed

+251
-21
lines changed

.changeset/perfect-turtles-reply.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'react-docgen': minor
3+
---
4+
5+
Support `PropsWithoutRef`, `PropsWithRef` and `PropsWithChildren` in TypeScript.
6+
7+
Component props are now detected correctly when these builtin types are used,
8+
but they do currently not add any props to the documentation.

packages/react-docgen/src/handlers/codeTypeHandler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ function setPropDescriptor(
9898
}
9999

100100
/**
101-
* This handler tries to find flow Type annotated react components and extract
101+
* This handler tries to find flow and TS Type annotated react components and extract
102102
* its types to the documentation. It also extracts docblock comments which are
103103
* inlined in the type definition.
104104
*/

packages/react-docgen/src/utils/__tests__/__snapshots__/getTypeFromReactComponent-test.ts.snap

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,18 @@ exports[`getTypeFromReactComponent > TypeScript > stateless > finds variable typ
258258
]
259259
`;
260260

261+
exports[`getTypeFromReactComponent > TypeScript > stateless > finds wrapped param type annotation 1`] = `
262+
[
263+
Node {
264+
"type": "TSTypeReference",
265+
"typeName": Node {
266+
"name": "Props",
267+
"type": "Identifier",
268+
},
269+
},
270+
]
271+
`;
272+
261273
exports[`getTypeFromReactComponent > handles no class props 1`] = `[]`;
262274

263275
exports[`getTypeFromReactComponent > handles no stateless props 1`] = `[]`;
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2+
3+
exports[`unwrapBuiltinTSPropTypes > React.PropsWithChildren 1`] = `
4+
Node {
5+
"type": "TSTypeReference",
6+
"typeName": Node {
7+
"name": "Props",
8+
"type": "Identifier",
9+
},
10+
}
11+
`;
12+
13+
exports[`unwrapBuiltinTSPropTypes > React.PropsWithRef 1`] = `
14+
Node {
15+
"type": "TSTypeReference",
16+
"typeName": Node {
17+
"name": "Props",
18+
"type": "Identifier",
19+
},
20+
}
21+
`;
22+
23+
exports[`unwrapBuiltinTSPropTypes > React.PropsWithoutRef 1`] = `
24+
Node {
25+
"type": "TSTypeReference",
26+
"typeName": Node {
27+
"name": "Props",
28+
"type": "Identifier",
29+
},
30+
}
31+
`;
32+
33+
exports[`unwrapBuiltinTSPropTypes > does not follow reassignment 1`] = `
34+
Node {
35+
"type": "TSTypeReference",
36+
"typeName": Node {
37+
"name": "bar",
38+
"type": "Identifier",
39+
},
40+
}
41+
`;
42+
43+
exports[`unwrapBuiltinTSPropTypes > multiple 1`] = `
44+
Node {
45+
"type": "TSTypeReference",
46+
"typeName": Node {
47+
"name": "Props",
48+
"type": "Identifier",
49+
},
50+
}
51+
`;
52+
53+
exports[`unwrapBuiltinTSPropTypes > with named import 1`] = `
54+
Node {
55+
"type": "TSTypeReference",
56+
"typeName": Node {
57+
"name": "Props",
58+
"type": "Identifier",
59+
},
60+
}
61+
`;
62+
63+
exports[`unwrapBuiltinTSPropTypes > with require 1`] = `
64+
Node {
65+
"type": "TSTypeReference",
66+
"typeName": Node {
67+
"name": "Props",
68+
"type": "Identifier",
69+
},
70+
}
71+
`;

packages/react-docgen/src/utils/__tests__/getTypeFromReactComponent-test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,18 @@ describe('getTypeFromReactComponent', () => {
4040
expect(getTypeFromReactComponent(path)).toMatchSnapshot();
4141
});
4242

43+
test('finds wrapped param type annotation', () => {
44+
const path = parseTypescript
45+
.statementLast<VariableDeclaration>(
46+
`import React from 'react';
47+
const x = (props: React.PropsWithChildren<Props>) => {}`,
48+
)
49+
.get('declarations')[0]
50+
.get('init') as NodePath<ArrowFunctionExpression>;
51+
52+
expect(getTypeFromReactComponent(path)).toMatchSnapshot();
53+
});
54+
4355
test('finds param inline type', () => {
4456
const path = parseTypescript
4557
.statementLast<VariableDeclaration>(
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import type { TSTypeReference, VariableDeclaration } from '@babel/types';
2+
import { parseTypescript } from '../../../tests/utils';
3+
import unwrapBuiltinTSPropTypes from '../unwrapBuiltinTSPropTypes.js';
4+
import { describe, expect, test } from 'vitest';
5+
import type { NodePath } from '@babel/traverse';
6+
7+
describe('unwrapBuiltinTSPropTypes', () => {
8+
test('React.PropsWithChildren', () => {
9+
const path = parseTypescript
10+
.statementLast<VariableDeclaration>(
11+
`import React from 'react';
12+
var foo: React.PropsWithChildren<Props>`,
13+
)
14+
.get(
15+
'declarations.0.id.typeAnnotation.typeAnnotation',
16+
) as NodePath<TSTypeReference>;
17+
18+
expect(unwrapBuiltinTSPropTypes(path)).toMatchSnapshot();
19+
});
20+
21+
test('React.PropsWithoutRef', () => {
22+
const path = parseTypescript
23+
.statementLast<VariableDeclaration>(
24+
`import React from 'react';
25+
var foo: React.PropsWithoutRef<Props>`,
26+
)
27+
.get(
28+
'declarations.0.id.typeAnnotation.typeAnnotation',
29+
) as NodePath<TSTypeReference>;
30+
31+
expect(unwrapBuiltinTSPropTypes(path)).toMatchSnapshot();
32+
});
33+
34+
test('React.PropsWithRef', () => {
35+
const path = parseTypescript
36+
.statementLast<VariableDeclaration>(
37+
`import React from 'react';
38+
var foo: React.PropsWithRef<Props>`,
39+
)
40+
.get(
41+
'declarations.0.id.typeAnnotation.typeAnnotation',
42+
) as NodePath<TSTypeReference>;
43+
44+
expect(unwrapBuiltinTSPropTypes(path)).toMatchSnapshot();
45+
});
46+
47+
test('multiple', () => {
48+
const path = parseTypescript
49+
.statementLast<VariableDeclaration>(
50+
`import React from 'react';
51+
var foo: React.PropsWithChildren<React.PropsWithRef<Props>>`,
52+
)
53+
.get(
54+
'declarations.0.id.typeAnnotation.typeAnnotation',
55+
) as NodePath<TSTypeReference>;
56+
57+
expect(unwrapBuiltinTSPropTypes(path)).toMatchSnapshot();
58+
});
59+
60+
test('does not follow reassignment', () => {
61+
const path = parseTypescript
62+
.statementLast<VariableDeclaration>(
63+
`import React from 'react';
64+
type bar = React.PropsWithRef<Props>
65+
var foo: React.PropsWithChildren<bar>`,
66+
)
67+
.get(
68+
'declarations.0.id.typeAnnotation.typeAnnotation',
69+
) as NodePath<TSTypeReference>;
70+
71+
expect(unwrapBuiltinTSPropTypes(path)).toMatchSnapshot();
72+
});
73+
74+
test('with require', () => {
75+
const path = parseTypescript
76+
.statementLast<VariableDeclaration>(
77+
`const React = require('react');
78+
var foo: React.PropsWithRef<Props>`,
79+
)
80+
.get(
81+
'declarations.0.id.typeAnnotation.typeAnnotation',
82+
) as NodePath<TSTypeReference>;
83+
84+
expect(unwrapBuiltinTSPropTypes(path)).toMatchSnapshot();
85+
});
86+
87+
test('with named import', () => {
88+
const path = parseTypescript
89+
.statementLast<VariableDeclaration>(
90+
`import { PropsWithRef } from 'react';
91+
var foo: PropsWithRef<Props>`,
92+
)
93+
.get(
94+
'declarations.0.id.typeAnnotation.typeAnnotation',
95+
) as NodePath<TSTypeReference>;
96+
97+
expect(unwrapBuiltinTSPropTypes(path)).toMatchSnapshot();
98+
});
99+
});

packages/react-docgen/src/utils/getTypeFromReactComponent.ts

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import type {
2323
} from '@babel/types';
2424
import getTypeIdentifier from './getTypeIdentifier.js';
2525
import isReactBuiltinReference from './isReactBuiltinReference.js';
26+
import unwrapBuiltinTSPropTypes from './unwrapBuiltinTSPropTypes.js';
2627

2728
// TODO TESTME
2829

@@ -62,13 +63,11 @@ function findAssignedVariableType(
6263
isReactBuiltinReference(typeName, 'VoidFunctionComponent') ||
6364
isReactBuiltinReference(typeName, 'VFC')
6465
) {
65-
const typeParameters = typeAnnotation.get(
66-
'typeParameters',
67-
) as NodePath<TSTypeParameterInstantiation>;
66+
const typeParameters = typeAnnotation.get('typeParameters');
6867

69-
if (!typeParameters.hasNode()) return null;
70-
71-
return typeParameters.get('params')[0] ?? null;
68+
if (typeParameters.hasNode()) {
69+
return typeParameters.get('params')[0] ?? null;
70+
}
7271
}
7372
}
7473

@@ -106,27 +105,25 @@ export default (componentDefinition: NodePath): NodePath[] => {
106105
typePaths.push(typeAnnotation);
107106
}
108107
}
108+
} else {
109+
const propsParam = getStatelessPropsPath(componentDefinition);
109110

110-
return typePaths;
111-
}
112-
113-
const propsParam = getStatelessPropsPath(componentDefinition);
114-
115-
if (propsParam) {
116-
const typeAnnotation = getTypeAnnotation(propsParam);
111+
if (propsParam) {
112+
const typeAnnotation = getTypeAnnotation(propsParam);
117113

118-
if (typeAnnotation) {
119-
typePaths.push(typeAnnotation);
114+
if (typeAnnotation) {
115+
typePaths.push(typeAnnotation);
116+
}
120117
}
121-
}
122118

123-
const assignedVariableType = findAssignedVariableType(componentDefinition);
119+
const assignedVariableType = findAssignedVariableType(componentDefinition);
124120

125-
if (assignedVariableType) {
126-
typePaths.push(assignedVariableType);
121+
if (assignedVariableType) {
122+
typePaths.push(assignedVariableType);
123+
}
127124
}
128125

129-
return typePaths;
126+
return typePaths.map((typePath) => unwrapBuiltinTSPropTypes(typePath));
130127
};
131128

132129
export function applyToTypeProperties(
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import type { NodePath } from '@babel/traverse';
2+
import isReactBuiltinReference from './isReactBuiltinReference.js';
3+
4+
/**
5+
* Unwraps NodePaths from the builtin TS types `PropsWithoutRef`,
6+
* `PropsWithRef` and `PropsWithChildren` and returns the inner type param.
7+
* If none of the builtin types is detected the path is returned as-is
8+
*/
9+
export default function unwrapBuiltinTSPropTypes(typePath: NodePath): NodePath {
10+
if (typePath.isTSTypeReference()) {
11+
const typeName = typePath.get('typeName');
12+
13+
if (
14+
isReactBuiltinReference(typeName, 'PropsWithoutRef') ||
15+
isReactBuiltinReference(typeName, 'PropsWithRef') ||
16+
isReactBuiltinReference(typeName, 'PropsWithChildren')
17+
) {
18+
const typeParameters = typePath.get('typeParameters');
19+
20+
if (typeParameters.hasNode()) {
21+
const innerType = typeParameters.get('params')[0];
22+
23+
if (innerType) {
24+
return unwrapBuiltinTSPropTypes(innerType);
25+
}
26+
}
27+
}
28+
}
29+
30+
return typePath;
31+
}

0 commit comments

Comments
 (0)