Skip to content

Commit 2117fb7

Browse files
committed
Fix squashing props in generic overloaded components
1 parent 1850853 commit 2117fb7

File tree

9 files changed

+321
-61
lines changed

9 files changed

+321
-61
lines changed

src/models/property.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,5 @@ export class PropertyNode {
77
public type: TypeNode,
88
public documentation: Documentation | undefined,
99
public optional: boolean,
10-
id: number | undefined,
11-
) {
12-
this.$$id = id;
13-
}
14-
15-
/** @internal */
16-
public readonly $$id: number | undefined;
10+
) {}
1711
}

src/models/types/compoundTypeUtils.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { ReferenceNode } from './reference';
55
import { TypeNode } from '../node';
66
import { IntersectionNode } from './intersection';
77
import { UnionNode } from './union';
8+
import { TypeParameterNode } from './typeParameter';
89

910
export function flattenTypes(
1011
nodes: readonly TypeNode[],
@@ -28,7 +29,7 @@ export function deduplicateMemberTypes(types: TypeNode[]): TypeNode[] {
2829
return x.value;
2930
}
3031

31-
if (x instanceof ReferenceNode) {
32+
if (x instanceof ReferenceNode || x instanceof TypeParameterNode) {
3233
return x.name;
3334
}
3435

src/parsers/componentParser.ts

Lines changed: 39 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -53,26 +53,13 @@ function hasReactNodeLikeReturnType(type: FunctionNode) {
5353

5454
function squashComponentProps(callSignatures: CallSignature[], context: ParserContext) {
5555
// squash props
56-
// { variant: 'a', href: string } & { variant: 'b' }
56+
// { variant: 'a', href: string } | { variant: 'b' }
5757
// to
5858
// { variant: 'a' | 'b', href?: string }
59-
const props: Record<string, PropertyNode> = {};
59+
const props: Map<string, PropertyNode> = new Map<string, PropertyNode>();
6060
const usedPropsPerSignature: Set<string>[] = [];
6161

62-
function unwrapUnionType(type: UnionNode): ObjectNode[] {
63-
return type.types
64-
.map((type) => {
65-
if (type instanceof ObjectNode) {
66-
return type;
67-
} else if (type instanceof UnionNode) {
68-
return unwrapUnionType(type);
69-
}
70-
})
71-
.flat()
72-
.filter((t) => !!t);
73-
}
74-
75-
const allParametersUnionMembers = callSignatures
62+
const propsFromCallSignatures = callSignatures
7663
.map((signature) => {
7764
const propsParameter = signature.parameters[0];
7865
if (!propsParameter) {
@@ -94,44 +81,61 @@ function squashComponentProps(callSignatures: CallSignature[], context: ParserCo
9481
.flat()
9582
.filter((t) => !!t);
9683

97-
allParametersUnionMembers.forEach((propUnionMember) => {
84+
propsFromCallSignatures.forEach((propsObject) => {
9885
const usedProps: Set<string> = new Set();
9986

100-
propUnionMember.properties.forEach((propNode) => {
87+
propsObject.properties.forEach((propNode) => {
10188
usedProps.add(propNode.name);
10289

103-
let { [propNode.name]: currentTypeNode } = props;
104-
if (currentTypeNode === undefined) {
105-
currentTypeNode = propNode;
106-
} else if (currentTypeNode.$$id !== propNode.$$id) {
107-
const mergedPropType = new UnionNode(undefined, [], [currentTypeNode.type, propNode.type]);
108-
109-
currentTypeNode = new PropertyNode(
110-
currentTypeNode.name,
90+
// Check if a prop with a given name has already been encountered.
91+
const existingPropNode = props.get(propNode.name);
92+
if (existingPropNode === undefined) {
93+
// If not, we can just add it.
94+
props.set(propNode.name, propNode);
95+
} else {
96+
// If it has, we need to merge the types in a union.
97+
// If both prop objects define the prop with the same type, the UnionNode constructor will deduplicate them.
98+
const mergedPropType = new UnionNode(undefined, [], [existingPropNode.type, propNode.type]);
99+
100+
// If the current prop is optional, the whole union will be optional.
101+
const mergedPropNode = new PropertyNode(
102+
existingPropNode.name,
111103
mergedPropType.types.length === 1 ? mergedPropType.types[0] : mergedPropType,
112-
currentTypeNode.documentation,
113-
currentTypeNode.optional || propNode.optional,
114-
undefined,
104+
existingPropNode.documentation,
105+
existingPropNode.optional || propNode.optional,
115106
);
116-
}
117107

118-
props[propNode.name] = currentTypeNode;
108+
props.set(propNode.name, mergedPropNode);
109+
}
119110
});
120111

121112
usedPropsPerSignature.push(usedProps);
122113
});
123114

124-
return Object.entries(props).map(([name, property]) => {
115+
// If a prop is used in some signatures, but not in others, we need to mark it as optional.
116+
return [...props.entries()].map(([name, property]) => {
125117
const onlyUsedInSomeSignatures = usedPropsPerSignature.some((props) => !props.has(name));
126118
if (onlyUsedInSomeSignatures) {
127-
// mark as optional
128119
return markPropertyAsOptional(property, context);
129120
}
130121

131122
return property;
132123
});
133124
}
134125

126+
function unwrapUnionType(type: UnionNode): ObjectNode[] {
127+
return type.types
128+
.map((type) => {
129+
if (type instanceof ObjectNode) {
130+
return type;
131+
} else if (type instanceof UnionNode) {
132+
return unwrapUnionType(type);
133+
}
134+
})
135+
.flat()
136+
.filter((t) => !!t);
137+
}
138+
135139
function markPropertyAsOptional(property: PropertyNode, context: ParserContext) {
136140
const canBeUndefined =
137141
property.type instanceof UnionNode &&
@@ -142,8 +146,8 @@ function markPropertyAsOptional(property: PropertyNode, context: ParserContext)
142146
const { compilerOptions } = context;
143147
if (!canBeUndefined && !compilerOptions.exactOptionalPropertyTypes) {
144148
const newType = new UnionNode(undefined, [], [property.type, new IntrinsicNode('undefined')]);
145-
return new PropertyNode(property.name, newType, property.documentation, true, undefined);
149+
return new PropertyNode(property.name, newType, property.documentation, true);
146150
}
147151

148-
return new PropertyNode(property.name, property.type, property.documentation, true, undefined);
152+
return new PropertyNode(property.name, property.type, property.documentation, true);
149153
}

src/parsers/propertyParser.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,8 +36,8 @@ export function parseProperty(
3636
skipResolvingComplexTypes,
3737
);
3838

39-
// Typechecker only gives the type "any" if it's present in a union
40-
// This means the type of "a" in {a?:any} isn't "any | undefined"
39+
// Typechecker only gives the type "any" if it's present in a union.
40+
// This means the type of `a` in `{ a?: any }` isn't `any | undefined`.
4141
// So instead we check for the questionmark to detect optional types
4242
if ((type.flags & ts.TypeFlags.Any || type.flags & ts.TypeFlags.Unknown) && propertySignature) {
4343
isOptional = Boolean(propertySignature.questionToken);
@@ -50,8 +50,6 @@ export function parseProperty(
5050
parsedType,
5151
getDocumentationFromSymbol(propertySymbol, checker),
5252
isOptional,
53-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
54-
(propertySymbol as any).id,
5553
);
5654
} catch (error) {
5755
if (!(error instanceof ParserError)) {
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
export function Component<Value>(props: Props1<Value>): React.ReactNode;
2+
export function Component<Value>(props: Props2<Value>): React.ReactNode;
3+
export function Component<Value>(props: Props1<Value> | Props2<Value>): React.ReactNode {
4+
return null;
5+
}
6+
7+
interface Props1<Value> {
8+
discriminant?: false | undefined;
9+
variant1Prop: Value;
10+
variant1OptionalProp?: Value;
11+
mandatoryProp: Value;
12+
}
13+
14+
interface Props2<Value> {
15+
discriminant: true;
16+
variant2Prop: Value;
17+
variant2OptionalProp?: Value;
18+
mandatoryProp: Value;
19+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
{
2+
"name": "test/component-function-overloads-generic/input",
3+
"exports": [
4+
{
5+
"name": "Component",
6+
"type": {
7+
"kind": "component",
8+
"name": "Component",
9+
"parentNamespaces": [],
10+
"props": [
11+
{
12+
"name": "discriminant",
13+
"type": {
14+
"kind": "union",
15+
"types": [
16+
{
17+
"kind": "intrinsic",
18+
"parentNamespaces": [],
19+
"intrinsic": "boolean"
20+
},
21+
{
22+
"kind": "intrinsic",
23+
"parentNamespaces": [],
24+
"intrinsic": "undefined"
25+
}
26+
]
27+
},
28+
"optional": true
29+
},
30+
{
31+
"name": "variant1Prop",
32+
"type": {
33+
"kind": "union",
34+
"types": [
35+
{
36+
"kind": "typeParameter",
37+
"name": "Value",
38+
"parentNamespaces": []
39+
},
40+
{
41+
"kind": "intrinsic",
42+
"parentNamespaces": [],
43+
"intrinsic": "undefined"
44+
}
45+
]
46+
},
47+
"optional": true
48+
},
49+
{
50+
"name": "variant1OptionalProp",
51+
"type": {
52+
"kind": "union",
53+
"types": [
54+
{
55+
"kind": "typeParameter",
56+
"name": "Value",
57+
"parentNamespaces": []
58+
},
59+
{
60+
"kind": "intrinsic",
61+
"parentNamespaces": [],
62+
"intrinsic": "undefined"
63+
}
64+
]
65+
},
66+
"optional": true
67+
},
68+
{
69+
"name": "mandatoryProp",
70+
"type": {
71+
"kind": "typeParameter",
72+
"name": "Value",
73+
"parentNamespaces": []
74+
},
75+
"optional": false
76+
},
77+
{
78+
"name": "variant2Prop",
79+
"type": {
80+
"kind": "union",
81+
"types": [
82+
{
83+
"kind": "typeParameter",
84+
"name": "Value",
85+
"parentNamespaces": []
86+
},
87+
{
88+
"kind": "intrinsic",
89+
"parentNamespaces": [],
90+
"intrinsic": "undefined"
91+
}
92+
]
93+
},
94+
"optional": true
95+
},
96+
{
97+
"name": "variant2OptionalProp",
98+
"type": {
99+
"kind": "union",
100+
"types": [
101+
{
102+
"kind": "typeParameter",
103+
"name": "Value",
104+
"parentNamespaces": []
105+
},
106+
{
107+
"kind": "intrinsic",
108+
"parentNamespaces": [],
109+
"intrinsic": "undefined"
110+
}
111+
]
112+
},
113+
"optional": true
114+
}
115+
]
116+
}
117+
}
118+
]
119+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
export function Component(props: Props1): React.ReactNode;
2+
export function Component(props: Props2): React.ReactNode;
3+
export function Component(props: Props1 | Props2): React.ReactNode {
4+
return null;
5+
}
6+
7+
interface Props1 {
8+
discriminant?: false | undefined;
9+
variant1Prop: string;
10+
variant1OptionalProp?: string;
11+
mandatoryProp: string;
12+
}
13+
14+
interface Props2 {
15+
discriminant: true;
16+
variant2Prop: string;
17+
variant2OptionalProp?: string;
18+
mandatoryProp: string;
19+
}

0 commit comments

Comments
 (0)