Skip to content

Commit

Permalink
Support React.AbstractComponent TS conversion
Browse files Browse the repository at this point in the history
Summary:
Support translating `React.AbstractComponent` into the appropriate TS version. In this case i have chosen to convert to `React.ForwardRefExoticComponent`, e.g.
- `React.AbstractComponent<Config>` -> `React.ForwardRefExoticComponent<Config>`
- `React.AbstractComponent<Config, Instance>` -> `React.ForwardRefExoticComponent<Config & React.RefAttributes<Instance>>`

I chose `ForwardRefExoticComponent` even in the non `Instance` case as its the most flexible of the TS `ExoticComponent` API's. I'm interested how this works in real code, we may need to change it. TS React API's: https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react/v17/index.d.ts#L349-L363

Reviewed By: evanyeung

Differential Revision: D43523533

fbshipit-source-id: 79845a2ec883c2fa4eea5c23007e142aa21c1a4a
  • Loading branch information
pieterv authored and facebook-github-bot committed Mar 3, 2023
1 parent 1b759f4 commit 4171902
Show file tree
Hide file tree
Showing 4 changed files with 180 additions and 66 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ exports[`flowDefToTSDef export/declare/default/identifier/member 1`] = `

import * as React from 'react';
type Props = {a: string};
declare const $$EXPORT_DEFAULT_DECLARATION$$: React.AbstractComponent<Props>;
declare const $$EXPORT_DEFAULT_DECLARATION$$: React.ForwardRefExoticComponent<Props>;
export default $$EXPORT_DEFAULT_DECLARATION$$;
"
`;
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@

import * as React from 'react';

type T2 = React.Node; // React.ReactNode
type T1 = React.MixedElement; // JSX.Element
type T2 = React.Element<typeof Component>; // React.ReactElement<typeof Component>
type T1 = React.Node; // React.ReactNode
type T2 = React.MixedElement; // JSX.Element
type T3 = React.Element<typeof Component>; // React.ReactElement<typeof Component>
type T4 = React.AbstractComponent<Props>; // React.ForwardRefExoticComponent<Props>
type T4 = React.AbstractComponent<Props, HTMLElement>; // React.ForwardRefExoticComponent<Props & React.RefAttributes<HTMLElement>>

type Props = {A: string};
declare function Component(props: Props): React.Node;
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,13 @@ exports[`flowDefToTSDef types/react 1`] = `
*/

import * as React from 'react';
type T2 = React.ReactNode;
type T1 = JSX.Element;
type T2 = React.ReactElement<typeof Component>;
type T1 = React.ReactNode;
type T2 = JSX.Element;
type T3 = React.ReactElement<typeof Component>;
type T4 = React.ForwardRefExoticComponent<Props>;
type T4 = React.ForwardRefExoticComponent<
Props & React.RefAttributes<HTMLElement>
>;
type Props = {A: string};
declare function Component(props: Props): React.ReactNode;
"
Expand Down
226 changes: 167 additions & 59 deletions tools/hermes-parser/js/hermes-translate/src/flowDefToTSDef.js
Original file line number Diff line number Diff line change
Expand Up @@ -1222,42 +1222,61 @@ const getTransforms = (code: string, scopeManager: ScopeManager) => {
GenericTypeAnnotation(
node: FlowESTree.GenericTypeAnnotation,
): TSESTree.TypeNode {
if (node.id.type !== 'Identifier') {
return {
type: 'TSTypeReference',
typeName: transform.QualifiedTypeIdentifier(node.id),
typeParameters:
node.typeParameters == null
? undefined
: transform.TypeParameterInstantiation(node.typeParameters),
};
}
const [fullTypeName, baseId] = (() => {
let names = [];
let currentNode = node.id;

while (currentNode != null) {
switch (currentNode.type) {
case 'Identifier': {
names.unshift(currentNode.name);
return [names.join('.'), currentNode];
}
case 'QualifiedTypeIdentifier': {
names.unshift(currentNode.id.name);
currentNode = currentNode.qualification;
break;
}
}
}

throw translationError(
node,
`Invalid program state, types should only contain 'Identifier' and 'QualifiedTypeIdentifier' nodes.`,
);
})();

// attempt to handle any of flow's utilitiy types
const originalTypeName = node.id.name;
const assertHasExactlyNTypeParameters = (
count: number,
): $ReadOnlyArray<TSESTree.TypeNode> => {
if (
node.typeParameters == null ||
node.typeParameters.params.length !== count
) {
if (node.typeParameters != null) {
if (node.typeParameters.params.length !== count) {
throw translationError(
node,
`Expected exactly ${count} type parameter${
count > 1 ? 's' : ''
} with \`${fullTypeName}\``,
);
}

const res = [];
for (const param of node.typeParameters.params) {
res.push(transform.TypeAnnotationType(param));
}
return res;
}

if (count !== 0) {
throw translationError(
node,
`Expected exactly ${count} type parameter${
count > 1 ? 's' : ''
} with \`${originalTypeName}\``,
`Expected no type parameters with \`${fullTypeName}\``,
);
}

const res = [];
for (const param of node.typeParameters.params) {
res.push(transform.TypeAnnotationType(param));
}
return res;
return [];
};

switch (originalTypeName) {
switch (fullTypeName) {
case '$Call':
case '$ObjMap':
case '$ObjMapConst':
Expand Down Expand Up @@ -1285,7 +1304,7 @@ const getTransforms = (code: string, scopeManager: ScopeManager) => {
type PropType = ReturnType<ExtractPropType<Obj>>; // number
```
*/
throw unsupportedTranslationError(node, originalTypeName);
throw unsupportedTranslationError(node, fullTypeName);
}

case '$Diff':
Expand Down Expand Up @@ -1483,7 +1502,7 @@ const getTransforms = (code: string, scopeManager: ScopeManager) => {
case '$Supertype': {
// These types are deprecated and shouldn't be used in any modern code
// so let's not even bother trying to figure it out
throw unsupportedTranslationError(node, originalTypeName);
throw unsupportedTranslationError(node, fullTypeName);
}

case '$Values': {
Expand Down Expand Up @@ -1539,6 +1558,127 @@ const getTransforms = (code: string, scopeManager: ScopeManager) => {
}
}

// React special conversion:
if (isReactImport(baseId)) {
switch (fullTypeName) {
// React.Node -> React.ReactNode
case 'React.Node': {
assertHasExactlyNTypeParameters(0);
return {
type: 'TSTypeReference',
typeName: {
type: 'TSQualifiedName',
left: transform.Identifier(baseId, false),
right: {
type: 'Identifier',
name: `ReactNode`,
},
},
typeParameters: undefined,
};
}
// React.Element<typeof Component> -> React.ReactElement<typeof Component>
case 'React.Element': {
return {
type: 'TSTypeReference',
typeName: {
type: 'TSQualifiedName',
left: transform.Identifier(baseId, false),
right: {
type: 'Identifier',
name: `ReactElement`,
},
},
typeParameters: {
type: 'TSTypeParameterInstantiation',
params: assertHasExactlyNTypeParameters(1),
},
};
}
// React.MixedElement -> JSX.Element
case 'React.MixedElement': {
assertHasExactlyNTypeParameters(0);
return {
type: 'TSTypeReference',
typeName: {
type: 'TSQualifiedName',
left: {
type: 'Identifier',
name: 'JSX',
},
right: {
type: 'Identifier',
name: 'Element',
},
},
typeParameters: undefined,
};
}
// React.AbstractComponent<Config> -> React.ForwardRefExoticComponent<Config>
// React.AbstractComponent<Config, Instance> -> React.ForwardRefExoticComponent<Config & React.RefAttributes<Instance>>
case 'React.AbstractComponent': {
const typeParameters = node.typeParameters;
if (typeParameters == null || typeParameters.params.length === 0) {
throw translationError(
node,
`Expected at least 1 type parameter with \`${fullTypeName}\``,
);
}
const params = typeParameters.params;
if (params.length > 2) {
throw translationError(
node,
`Expected at no more than 2 type parameters with \`${fullTypeName}\``,
);
}

let newTypeParam = transform.TypeAnnotationType(params[0]);
if (params[1] != null) {
newTypeParam = {
type: 'TSIntersectionType',
types: [
newTypeParam,
{
type: 'TSTypeReference',
typeName: {
type: 'TSQualifiedName',
left: {
type: 'Identifier',
name: 'React',
},
right: {
type: 'Identifier',
name: 'RefAttributes',
},
},
typeParameters: {
type: 'TSTypeParameterInstantiation',
params: [transform.TypeAnnotationType(params[1])],
},
},
],
};
}

return {
type: 'TSTypeReference',
typeName: {
type: 'TSQualifiedName',
left: transform.Identifier(baseId, false),
right: {
type: 'Identifier',
name: `ForwardRefExoticComponent`,
},
},
typeParameters: {
type: 'TSTypeParameterInstantiation',
params: [newTypeParam],
},
};
}
}
}

return {
type: 'TSTypeReference',
typeName:
Expand Down Expand Up @@ -2113,38 +2253,6 @@ const getTransforms = (code: string, scopeManager: ScopeManager) => {
): TSESTree.TSQualifiedName {
const qual = node.qualification;

// React special conversion:
if (qual.type === 'Identifier' && isReactImport(qual)) {
switch (node.id.name) {
// React.Something -> React.ReactSomething
case 'Element':
case 'Node': {
return {
type: 'TSQualifiedName',
left: transform.Identifier(qual, false),
right: {
type: 'Identifier',
name: `React${node.id.name}`,
},
};
}
// React.MixedElement -> JSX.Element
case 'MixedElement': {
return {
type: 'TSQualifiedName',
left: {
type: 'Identifier',
name: 'JSX',
},
right: {
type: 'Identifier',
name: 'Element',
},
};
}
}
}

return {
type: 'TSQualifiedName',
left:
Expand Down

0 comments on commit 4171902

Please sign in to comment.