diff --git a/src/utils/__tests__/getFlowType-test.js b/src/utils/__tests__/getFlowType-test.js index 675c7919164..4e6c9202a00 100644 --- a/src/utils/__tests__/getFlowType-test.js +++ b/src/utils/__tests__/getFlowType-test.js @@ -248,4 +248,34 @@ describe('getFlowType', () => { it(type, () => test(type, types[type])); }); }); + + it('resolves $Keys to union', () => { + var typePath = statement(` + var x: $Keys = 2; + const CONTENTS = { + 'apple': '🍎', + 'banana': '🍌', + }; + `).get('declarations', 0).get('id').get('typeAnnotation').get('typeAnnotation'); + + expect(getFlowType(typePath)).toEqual({name: 'union', elements: [ + { name: 'literal', value: "'apple'" }, + { name: 'literal', value: "'banana'" }, + ], raw: '$Keys'}); + }); + + it('resolves $Keys without typeof to union', () => { + var typePath = statement(` + var x: $Keys = 2; + const CONTENTS = { + 'apple': '🍎', + 'banana': '🍌', + }; + `).get('declarations', 0).get('id').get('typeAnnotation').get('typeAnnotation'); + + expect(getFlowType(typePath)).toEqual({name: 'union', elements: [ + { name: 'literal', value: "'apple'" }, + { name: 'literal', value: "'banana'" }, + ], raw: '$Keys'}); + }); }); diff --git a/src/utils/getFlowType.js b/src/utils/getFlowType.js index e6209dc9e2d..a49ad29ac6c 100644 --- a/src/utils/getFlowType.js +++ b/src/utils/getFlowType.js @@ -17,6 +17,7 @@ import printValue from './printValue'; import recast from 'recast'; import getTypeAnnotation from '../utils/getTypeAnnotation'; import resolveToValue from '../utils/resolveToValue'; +import { resolveObjectExpressionToNameArray } from '../utils/resolveObjectKeysToArray'; const { types: { namedTypes: types } } = recast; @@ -54,7 +55,32 @@ function getFlowTypeWithRequirements(path: NodePath): FlowTypeDescriptor { return type; } +function handleKeysHelper(path: NodePath) { + let value = path.get('typeParameters', 'params', 0); + if (types.TypeofTypeAnnotation.check(value.node)) { + value = value.get('argument', 'id'); + } else { + value = value.get('id'); + } + const resolvedPath = resolveToValue(value); + if (resolvedPath && types.ObjectExpression.check(resolvedPath.node)) { + const keys = resolveObjectExpressionToNameArray(resolvedPath, true); + + if (keys) { + return { + name: 'union', + raw: printValue(path), + elements: keys.map(value => ({ name: 'literal', value })), + }; + } + } +} + function handleGenericTypeAnnotation(path: NodePath) { + if (path.node.id.name === '$Keys' && path.node.typeParameters) { + return handleKeysHelper(path); + } + let type; if (types.QualifiedTypeIdentifier.check(path.node.id)) { type = handleQualifiedTypeIdentifier(path.get('id')); diff --git a/src/utils/resolveObjectKeysToArray.js b/src/utils/resolveObjectKeysToArray.js index a9527a648c2..15242e6bd00 100644 --- a/src/utils/resolveObjectKeysToArray.js +++ b/src/utils/resolveObjectKeysToArray.js @@ -32,7 +32,7 @@ function isObjectKeysCall(node: ASTNode): bool { node.callee.property.name === 'keys'; } -function resolveObjectExpressionToNameArray(objectExpression: NodePath): ?Array { +export function resolveObjectExpressionToNameArray(objectExpression: NodePath, raw: boolean = false): ?Array { if ( types.ObjectExpression.check(objectExpression.value) && objectExpression.value.properties.every( @@ -53,7 +53,7 @@ function resolveObjectExpressionToNameArray(objectExpression: NodePath): ?Array< if (types.Property.check(prop)) { // Key is either Identifier or Literal - const name = prop.key.name || prop.key.value; + const name = prop.key.name || (raw ? prop.key.raw : prop.key.value); values.push(name); } else if (types.SpreadProperty.check(prop)) {