diff --git a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts index 5f421708af0..b67423e0a89 100644 --- a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts +++ b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts @@ -455,6 +455,88 @@ describe('resolveType', () => { }) }) + describe('generics', () => { + test('generic with type literal', () => { + expect( + resolve(` + type Props = T + defineProps>() + `).props + ).toStrictEqual({ + foo: ['String'] + }) + }) + + test('generic used in intersection', () => { + expect( + resolve(` + type Foo = { foo: string; } + type Bar = { bar: number; } + type Props = T & U & { baz: boolean } + defineProps>() + `).props + ).toStrictEqual({ + foo: ['String'], + bar: ['Number'], + baz: ['Boolean'] + }) + }) + + test('generic type /w generic type alias', () => { + expect( + resolve(` + type Aliased = Readonly> + type Props = Aliased + type Foo = { foo: string; } + defineProps>() + `).props + ).toStrictEqual({ + foo: ['String'] + }) + }) + + test('generic type /w aliased type literal', () => { + expect( + resolve(` + type Aliased = { foo: T } + defineProps>() + `).props + ).toStrictEqual({ + foo: ['String'] + }) + }) + + test('generic type /w interface', () => { + expect( + resolve(` + interface Props { + foo: T + } + type Foo = string + defineProps>() + `).props + ).toStrictEqual({ + foo: ['String'] + }) + }) + + test('generic from external-file', () => { + const files = { + '/foo.ts': 'export type P = { foo: T }' + } + const { props } = resolve( + ` + import { P } from './foo' + defineProps>() + `, + files + ) + expect(props).toStrictEqual({ + foo: ['String'] + }) + }) + }) + describe('external type imports', () => { test('relative ts', () => { const files = { diff --git a/packages/compiler-sfc/src/script/resolveType.ts b/packages/compiler-sfc/src/script/resolveType.ts index d9b4dd1cb8c..c5f7681a6aa 100644 --- a/packages/compiler-sfc/src/script/resolveType.ts +++ b/packages/compiler-sfc/src/script/resolveType.ts @@ -118,7 +118,8 @@ interface ResolvedElements { export function resolveTypeElements( ctx: TypeResolveContext, node: Node & MaybeWithScope & { _resolvedElements?: ResolvedElements }, - scope?: TypeScope + scope?: TypeScope, + typeParameters?: Record ): ResolvedElements { if (node._resolvedElements) { return node._resolvedElements @@ -126,30 +127,37 @@ export function resolveTypeElements( return (node._resolvedElements = innerResolveTypeElements( ctx, node, - node._ownerScope || scope || ctxToScope(ctx) + node._ownerScope || scope || ctxToScope(ctx), + typeParameters )) } function innerResolveTypeElements( ctx: TypeResolveContext, node: Node, - scope: TypeScope + scope: TypeScope, + typeParameters?: Record ): ResolvedElements { switch (node.type) { case 'TSTypeLiteral': - return typeElementsToMap(ctx, node.members, scope) + return typeElementsToMap(ctx, node.members, scope, typeParameters) case 'TSInterfaceDeclaration': - return resolveInterfaceMembers(ctx, node, scope) + return resolveInterfaceMembers(ctx, node, scope, typeParameters) case 'TSTypeAliasDeclaration': case 'TSParenthesizedType': - return resolveTypeElements(ctx, node.typeAnnotation, scope) + return resolveTypeElements( + ctx, + node.typeAnnotation, + scope, + typeParameters + ) case 'TSFunctionType': { return { props: {}, calls: [node] } } case 'TSUnionType': case 'TSIntersectionType': return mergeElements( - node.types.map(t => resolveTypeElements(ctx, t, scope)), + node.types.map(t => resolveTypeElements(ctx, t, scope, typeParameters)), node.type ) case 'TSMappedType': @@ -171,20 +179,57 @@ function innerResolveTypeElements( scope.imports[typeName]?.source === 'vue' ) { return resolveExtractPropTypes( - resolveTypeElements(ctx, node.typeParameters.params[0], scope), + resolveTypeElements( + ctx, + node.typeParameters.params[0], + scope, + typeParameters + ), scope ) } const resolved = resolveTypeReference(ctx, node, scope) if (resolved) { - return resolveTypeElements(ctx, resolved, resolved._ownerScope) + const typeParams: Record = Object.create(null) + if ( + (resolved.type === 'TSTypeAliasDeclaration' || + resolved.type === 'TSInterfaceDeclaration') && + resolved.typeParameters && + node.typeParameters + ) { + resolved.typeParameters.params.forEach((p, i) => { + let param = typeParameters && typeParameters[p.name] + if (!param) param = node.typeParameters!.params[i] + typeParams[p.name] = param + }) + } + return resolveTypeElements( + ctx, + resolved, + resolved._ownerScope, + typeParams + ) } else { if (typeof typeName === 'string') { + if (typeParameters && typeParameters[typeName]) { + return resolveTypeElements( + ctx, + typeParameters[typeName], + scope, + typeParameters + ) + } if ( // @ts-ignore SupportedBuiltinsSet.has(typeName) ) { - return resolveBuiltin(ctx, node, typeName as any, scope) + return resolveBuiltin( + ctx, + node, + typeName as any, + scope, + typeParameters + ) } else if (typeName === 'ReturnType' && node.typeParameters) { // limited support, only reference types const ret = resolveReturnType( @@ -243,11 +288,17 @@ function innerResolveTypeElements( function typeElementsToMap( ctx: TypeResolveContext, elements: TSTypeElement[], - scope = ctxToScope(ctx) + scope = ctxToScope(ctx), + typeParameters?: Record ): ResolvedElements { const res: ResolvedElements = { props: {} } for (const e of elements) { if (e.type === 'TSPropertySignature' || e.type === 'TSMethodSignature') { + // capture generic parameters on node's scope + if (typeParameters) { + scope = createChildScope(scope) + Object.assign(scope.types, typeParameters) + } ;(e as MaybeWithScope)._ownerScope = scope const name = getId(e.key) if (name && !e.computed) { @@ -323,9 +374,15 @@ function createProperty( function resolveInterfaceMembers( ctx: TypeResolveContext, node: TSInterfaceDeclaration & MaybeWithScope, - scope: TypeScope + scope: TypeScope, + typeParameters?: Record ): ResolvedElements { - const base = typeElementsToMap(ctx, node.body.body, node._ownerScope) + const base = typeElementsToMap( + ctx, + node.body.body, + node._ownerScope, + typeParameters + ) if (node.extends) { for (const ext of node.extends) { if ( @@ -543,9 +600,15 @@ function resolveBuiltin( ctx: TypeResolveContext, node: TSTypeReference | TSExpressionWithTypeArguments, name: GetSetType, - scope: TypeScope + scope: TypeScope, + typeParameters?: Record ): ResolvedElements { - const t = resolveTypeElements(ctx, node.typeParameters!.params[0], scope) + const t = resolveTypeElements( + ctx, + node.typeParameters!.params[0], + scope, + typeParameters + ) switch (name) { case 'Partial': { const res: ResolvedElements = { props: {}, calls: t.calls } @@ -1103,14 +1166,7 @@ function moduleDeclToScope( return node._resolvedChildScope } - const scope = new TypeScope( - parentScope.filename, - parentScope.source, - parentScope.offset, - Object.create(parentScope.imports), - Object.create(parentScope.types), - Object.create(parentScope.declares) - ) + const scope = createChildScope(parentScope) if (node.body.type === 'TSModuleDeclaration') { const decl = node.body as TSModuleDeclaration & WithScope @@ -1124,6 +1180,17 @@ function moduleDeclToScope( return (node._resolvedChildScope = scope) } +function createChildScope(parentScope: TypeScope) { + return new TypeScope( + parentScope.filename, + parentScope.source, + parentScope.offset, + Object.create(parentScope.imports), + Object.create(parentScope.types), + Object.create(parentScope.declares) + ) +} + const importExportRE = /^Import|^Export/ function recordTypes( @@ -1262,7 +1329,7 @@ function recordType( if (overwriteId || node.id) types[overwriteId || getId(node.id!)] = node break case 'TSTypeAliasDeclaration': - types[node.id.name] = node.typeAnnotation + types[node.id.name] = node.typeParameters ? node : node.typeAnnotation break case 'TSDeclareFunction': if (node.id) declares[node.id.name] = node