diff --git a/packages/react-docgen/src/handlers/__tests__/__snapshots__/codeTypeHandler-test.ts.snap b/packages/react-docgen/src/handlers/__tests__/__snapshots__/codeTypeHandler-test.ts.snap
index 58dfd645a8c..feb9fe95148 100644
--- a/packages/react-docgen/src/handlers/__tests__/__snapshots__/codeTypeHandler-test.ts.snap
+++ b/packages/react-docgen/src/handlers/__tests__/__snapshots__/codeTypeHandler-test.ts.snap
@@ -1655,7 +1655,7 @@ exports[`codeTypeHandler > stateless TS component and variable type takes preced
"description": "",
"required": true,
"tsType": {
- "name": "string",
+ "name": "number | string",
},
},
}
diff --git a/packages/react-docgen/src/handlers/__tests__/codeTypeHandler-test.ts b/packages/react-docgen/src/handlers/__tests__/codeTypeHandler-test.ts
index 3bdfb497269..2f910688709 100644
--- a/packages/react-docgen/src/handlers/__tests__/codeTypeHandler-test.ts
+++ b/packages/react-docgen/src/handlers/__tests__/codeTypeHandler-test.ts
@@ -262,7 +262,7 @@ describe('codeTypeHandler', () => {
expect(documentation.descriptors).toMatchSnapshot();
});
- test('does not support union proptypes', () => {
+ test('does support union proptypes', () => {
const definition = parse
.statement(
`(props: Props) =>
;
@@ -274,7 +274,24 @@ describe('codeTypeHandler', () => {
.get('expression') as NodePath;
expect(() => codeTypeHandler(documentation, definition)).not.toThrow();
- expect(documentation.descriptors).toEqual({});
+ expect(documentation.descriptors).toEqual({
+ bar: {
+ required: true,
+ flowType: {
+ name: 'literal',
+ value: "'barValue'",
+ },
+ description: '',
+ },
+ foo: {
+ required: true,
+ flowType: {
+ name: 'literal',
+ value: "'fooValue'",
+ },
+ description: '',
+ },
+ });
});
describe('imported prop types', () => {
diff --git a/packages/react-docgen/src/handlers/codeTypeHandler.ts b/packages/react-docgen/src/handlers/codeTypeHandler.ts
index 75310b4b3d8..f008a9ba8e7 100644
--- a/packages/react-docgen/src/handlers/codeTypeHandler.ts
+++ b/packages/react-docgen/src/handlers/codeTypeHandler.ts
@@ -13,6 +13,7 @@ import type { NodePath } from '@babel/traverse';
import type { FlowType } from '@babel/types';
import type { ComponentNode } from '../resolver/index.js';
import type { Handler } from './index.js';
+import mergeTSIntersectionTypes from '../utils/mergeTSIntersectionTypes.js';
function setPropDescriptor(
documentation: Documentation,
@@ -80,15 +81,30 @@ function setPropDescriptor(
return;
}
const type = getTSType(typeAnnotation, typeParams);
-
const propName = getPropertyName(path);
if (!propName) return;
const propDescriptor = documentation.getPropDescriptor(propName);
- propDescriptor.required = !path.node.optional;
- propDescriptor.tsType = type;
+ if (propDescriptor.tsType) {
+ const mergedType = mergeTSIntersectionTypes(
+ {
+ name: propDescriptor.tsType.name,
+ required: propDescriptor.required,
+ },
+ {
+ name: type.name,
+ required: !path.node.optional,
+ },
+ );
+
+ propDescriptor.tsType.name = mergedType.name;
+ propDescriptor.required = mergedType.required;
+ } else {
+ propDescriptor.tsType = type;
+ propDescriptor.required = !path.node.optional;
+ }
// We are doing this here instead of in a different handler
// to not need to duplicate the logic for checking for
diff --git a/packages/react-docgen/src/utils/__tests__/__snapshots__/getTSType-test.ts.snap b/packages/react-docgen/src/utils/__tests__/__snapshots__/getTSType-test.ts.snap
index 22e588f60e3..1479e58e7ab 100644
--- a/packages/react-docgen/src/utils/__tests__/__snapshots__/getTSType-test.ts.snap
+++ b/packages/react-docgen/src/utils/__tests__/__snapshots__/getTSType-test.ts.snap
@@ -125,6 +125,36 @@ exports[`getTSType > can resolve indexed access to imported type 1`] = `
}
`;
+exports[`getTSType > deep resolve intersection types 1`] = `
+{
+ "elements": [
+ {
+ "key": "name",
+ "value": {
+ "name": "string",
+ "required": true,
+ },
+ },
+ {
+ "key": "a",
+ "value": {
+ "name": "number",
+ "required": true,
+ },
+ },
+ {
+ "key": "b",
+ "value": {
+ "name": "string",
+ "required": false,
+ },
+ },
+ ],
+ "name": "intersection",
+ "raw": "{ name: string } & (MyType | MySecondType)",
+}
+`;
+
exports[`getTSType > detects array type 1`] = `
{
"elements": [
diff --git a/packages/react-docgen/src/utils/__tests__/getTSType-test.ts b/packages/react-docgen/src/utils/__tests__/getTSType-test.ts
index 0413d2b8503..9e4659d3146 100644
--- a/packages/react-docgen/src/utils/__tests__/getTSType-test.ts
+++ b/packages/react-docgen/src/utils/__tests__/getTSType-test.ts
@@ -70,6 +70,12 @@ const mockImporter = makeMockImporter({
true,
).get('declaration') as NodePath,
+ MySecondType: (stmtLast) =>
+ stmtLast(
+ `export type MySecondType = { a: number, b?: never };`,
+ true,
+ ).get('declaration') as NodePath,
+
MyGenericType: (stmtLast) =>
stmtLast(
`export type MyGenericType = { a: T, b: Array };`,
@@ -501,6 +507,20 @@ describe('getTSType', () => {
expect(getTSType(typePath)).toMatchSnapshot();
});
+ test('deep resolve intersection types', () => {
+ const typePath = typeAlias(
+ `
+ const x: SuperType = {};
+ import { MyType } from 'MyType';
+ import { MySecondType } from 'MySecondType';
+ type SuperType = { name: string } & (MyType | MySecondType);
+ `,
+ mockImporter,
+ );
+
+ expect(getTSType(typePath)).toMatchSnapshot();
+ });
+
test('resolves typeof of import type', () => {
const typePath = typeAlias(
"var x: typeof import('MyType') = {};",
diff --git a/packages/react-docgen/src/utils/__tests__/mergeTSIntersectionTypes-test.ts b/packages/react-docgen/src/utils/__tests__/mergeTSIntersectionTypes-test.ts
new file mode 100644
index 00000000000..1430aa12b92
--- /dev/null
+++ b/packages/react-docgen/src/utils/__tests__/mergeTSIntersectionTypes-test.ts
@@ -0,0 +1,76 @@
+import { describe, expect, test } from 'vitest';
+import mergeTSIntersectionTypes from '../mergeTSIntersectionTypes.js';
+
+describe('mergeTSIntersectionTypes', () => {
+ test('it merges two types correctly', () => {
+ const mergedType = mergeTSIntersectionTypes(
+ {
+ name: 'string',
+ required: true,
+ },
+ {
+ name: 'number',
+ required: true,
+ },
+ );
+
+ expect(mergedType).toEqual({
+ name: 'string | number',
+ required: true,
+ });
+ });
+
+ test('it ignores types of "never"', () => {
+ const mergedType = mergeTSIntersectionTypes(
+ {
+ name: 'string',
+ required: true,
+ },
+ {
+ name: 'never',
+ required: true,
+ },
+ );
+
+ expect(mergedType).toEqual({
+ name: 'string',
+ required: true,
+ });
+ });
+
+ test('if one of the types is "unknown", it overrides all other types', () => {
+ const mergedType = mergeTSIntersectionTypes(
+ {
+ name: 'string',
+ required: true,
+ },
+ {
+ name: 'unknown',
+ required: true,
+ },
+ );
+
+ expect(mergedType).toEqual({
+ name: 'unknown',
+ required: true,
+ });
+ });
+
+ test('if one of the types is NOT required, the merged one is NOT required too', () => {
+ const mergedType = mergeTSIntersectionTypes(
+ {
+ name: 'string',
+ required: true,
+ },
+ {
+ name: 'number',
+ required: false,
+ },
+ );
+
+ expect(mergedType).toEqual({
+ name: 'string | number',
+ required: false,
+ });
+ });
+});
diff --git a/packages/react-docgen/src/utils/getTSType.ts b/packages/react-docgen/src/utils/getTSType.ts
index c938bf9470b..d1f2fd80fbf 100644
--- a/packages/react-docgen/src/utils/getTSType.ts
+++ b/packages/react-docgen/src/utils/getTSType.ts
@@ -36,8 +36,10 @@ import type {
TypeScript,
TSQualifiedName,
TSLiteralType,
+ TSParenthesizedType,
} from '@babel/types';
import { getDocblock } from './docblock.js';
+import mergeTSIntersectionTypes from './mergeTSIntersectionTypes.js';
const tsTypes: Record = {
TSAnyKeyword: 'any',
@@ -69,6 +71,7 @@ const namedTypes: Record<
TSUnionType: handleTSUnionType,
TSFunctionType: handleTSFunctionType,
TSIntersectionType: handleTSIntersectionType,
+ TSParenthesizedType: handleTSParenthesizedType,
TSMappedType: handleTSMappedType,
TSTupleType: handleTSTupleType,
TSTypeQuery: handleTSTypeQuery,
@@ -127,8 +130,7 @@ function handleTSTypeReference(
}
const resolvedPath =
- (typeParams && typeParams[type.name]) ||
- resolveToValue(path.get('typeName'));
+ (typeParams && typeParams[type.name]) || resolveToValue(typeName);
const typeParameters = path.get('typeParameters');
const resolvedTypeParameters = resolvedPath.get('typeParameters') as NodePath<
@@ -267,19 +269,109 @@ function handleTSUnionType(
};
}
+function handleTSParenthesizedType(
+ path: NodePath,
+ typeParams: TypeParameters | null,
+): ElementsType {
+ const innerTypePath = path.get('typeAnnotation');
+ const resolvedType = getTSTypeWithResolvedTypes(innerTypePath, typeParams);
+
+ return {
+ name: 'parenthesized',
+ raw: printValue(path),
+ elements: Array.isArray(resolvedType) ? resolvedType : [resolvedType],
+ };
+}
+
+interface PropertyWithKey {
+ key: TypeDescriptor | string;
+ value: TypeDescriptor;
+ description?: string | undefined;
+}
+
function handleTSIntersectionType(
path: NodePath,
typeParams: TypeParameters | null,
): ElementsType {
+ const resolvedTypes = path
+ .get('types')
+ .map((subTypePath) => getTSTypeWithResolvedTypes(subTypePath, typeParams));
+
+ let elements: Array> = [];
+
+ resolvedTypes.forEach((resolvedType) => {
+ switch (resolvedType.name) {
+ default:
+ case 'signature':
+ elements.push(resolvedType);
+ break;
+ case 'parenthesized': {
+ if ('elements' in resolvedType && resolvedType.elements[0]) {
+ const firstElement = resolvedType.elements[0];
+
+ if (firstElement && 'elements' in firstElement) {
+ elements = [...elements, ...firstElement.elements];
+ }
+ }
+ break;
+ }
+ }
+ });
+
+ const elementsDedup: PropertyWithKey[] = [];
+
+ // dedup elements
+ elements.forEach((element) => {
+ if (hasSignature(element)) {
+ const { signature } = element;
+
+ if (hasProperties(signature)) {
+ signature.properties.forEach((property) => {
+ const existingIndex = elementsDedup.findIndex(
+ ({ key }) => key === property.key,
+ );
+
+ if (existingIndex === -1) {
+ elementsDedup.push(property);
+ } else {
+ const existingProperty = elementsDedup[existingIndex];
+
+ if (existingProperty) {
+ elementsDedup[existingIndex] = {
+ key: property.key,
+ value: mergeTSIntersectionTypes(
+ existingProperty.value,
+ property.value,
+ ),
+ };
+ }
+ }
+ });
+ }
+ } else {
+ elementsDedup.push(element as unknown as PropertyWithKey);
+ }
+ });
+
return {
name: 'intersection',
raw: printValue(path),
- elements: path
- .get('types')
- .map((subType) => getTSTypeWithResolvedTypes(subType, typeParams)),
+ elements: elementsDedup as unknown as Array<
+ TypeDescriptor
+ >,
};
}
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+function hasSignature(element: any): element is { signature: unknown } {
+ return 'signature' in element;
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+function hasProperties(element: any): element is { properties: unknown } {
+ return 'properties' in element;
+}
+
// type OptionsFlags = { [Property in keyof Type]; };
function handleTSMappedType(
path: NodePath,
diff --git a/packages/react-docgen/src/utils/getTypeFromReactComponent.ts b/packages/react-docgen/src/utils/getTypeFromReactComponent.ts
index b0241c121a0..b1c7f8e87ca 100644
--- a/packages/react-docgen/src/utils/getTypeFromReactComponent.ts
+++ b/packages/react-docgen/src/utils/getTypeFromReactComponent.ts
@@ -162,9 +162,28 @@ export function applyToTypeProperties(
(typesPath) =>
applyToTypeProperties(documentation, typesPath, callback, typeParams),
);
- } else if (!path.isUnionTypeAnnotation()) {
- // The react-docgen output format does not currently allow
- // for the expression of union types
+ } else if (path.isParenthesizedExpression() || path.isTSParenthesizedType()) {
+ const typeAnnotation = path.get('typeAnnotation');
+ const typeAnnotationPath = Array.isArray(typeAnnotation)
+ ? typeAnnotation[0]
+ : typeAnnotation;
+
+ if (typeAnnotationPath) {
+ applyToTypeProperties(
+ documentation,
+ typeAnnotationPath,
+ callback,
+ typeParams,
+ );
+ }
+ } else if (path.isUnionTypeAnnotation() || path.isTSUnionType()) {
+ const typeNodes = path.get('types');
+ const types = Array.isArray(typeNodes) ? typeNodes : [typeNodes];
+
+ types.forEach((typesPath) =>
+ applyToTypeProperties(documentation, typesPath, callback, typeParams),
+ );
+ } else {
const typePath = resolveGenericTypeAnnotation(path);
if (typePath) {
diff --git a/packages/react-docgen/src/utils/mergeTSIntersectionTypes.ts b/packages/react-docgen/src/utils/mergeTSIntersectionTypes.ts
new file mode 100644
index 00000000000..41a88ab64b3
--- /dev/null
+++ b/packages/react-docgen/src/utils/mergeTSIntersectionTypes.ts
@@ -0,0 +1,60 @@
+import type {
+ TSFunctionSignatureType,
+ TypeDescriptor,
+} from '../Documentation.js';
+
+/**
+ * Merges two TSFunctionSignatureType types into one.
+ *
+ * @example
+ * const existingType = {
+ * "key": "children",
+ * "value": {
+ * "name": "ReactNode",
+ * "required": true,
+ * },
+ * };
+ * const newType = {
+ * "key": "children",
+ * "value": {
+ * "name": "never",
+ * "required": false,
+ * },
+ * };
+ *
+ * return {
+ * "key": "children",
+ * "value": {
+ * "name": "ReactNode",
+ * "required": false,
+ * },
+ * };
+ */
+export default (
+ existingType: TypeDescriptor,
+ newType: TypeDescriptor,
+): TypeDescriptor => {
+ const required =
+ newType.required === false || existingType.required === false
+ ? false
+ : existingType.required;
+
+ const existingTypesArray = existingType.name.split('|').map((t) => t.trim());
+ const existingTypes = new Set(existingTypesArray);
+
+ if (!['never'].includes(newType.name)) {
+ existingTypes.add(newType.name);
+ }
+
+ if (existingType.name === 'unknown' || newType.name === 'unknown') {
+ return {
+ name: 'unknown',
+ required,
+ };
+ }
+
+ return {
+ name: Array.from(existingTypes).join(' | '),
+ required,
+ };
+};