Skip to content

Commit eb5e307

Browse files
authoredDec 1, 2023
fix(compiler-sfc): support inferring generic types (#8511)
close #8482
1 parent 6345197 commit eb5e307

File tree

2 files changed

+173
-24
lines changed

2 files changed

+173
-24
lines changed
 

‎packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts

+82
Original file line numberDiff line numberDiff line change
@@ -455,6 +455,88 @@ describe('resolveType', () => {
455455
})
456456
})
457457

458+
describe('generics', () => {
459+
test('generic with type literal', () => {
460+
expect(
461+
resolve(`
462+
type Props<T> = T
463+
defineProps<Props<{ foo: string }>>()
464+
`).props
465+
).toStrictEqual({
466+
foo: ['String']
467+
})
468+
})
469+
470+
test('generic used in intersection', () => {
471+
expect(
472+
resolve(`
473+
type Foo = { foo: string; }
474+
type Bar = { bar: number; }
475+
type Props<T,U> = T & U & { baz: boolean }
476+
defineProps<Props<Foo, Bar>>()
477+
`).props
478+
).toStrictEqual({
479+
foo: ['String'],
480+
bar: ['Number'],
481+
baz: ['Boolean']
482+
})
483+
})
484+
485+
test('generic type /w generic type alias', () => {
486+
expect(
487+
resolve(`
488+
type Aliased<T> = Readonly<Partial<T>>
489+
type Props<T> = Aliased<T>
490+
type Foo = { foo: string; }
491+
defineProps<Props<Foo>>()
492+
`).props
493+
).toStrictEqual({
494+
foo: ['String']
495+
})
496+
})
497+
498+
test('generic type /w aliased type literal', () => {
499+
expect(
500+
resolve(`
501+
type Aliased<T> = { foo: T }
502+
defineProps<Aliased<string>>()
503+
`).props
504+
).toStrictEqual({
505+
foo: ['String']
506+
})
507+
})
508+
509+
test('generic type /w interface', () => {
510+
expect(
511+
resolve(`
512+
interface Props<T> {
513+
foo: T
514+
}
515+
type Foo = string
516+
defineProps<Props<Foo>>()
517+
`).props
518+
).toStrictEqual({
519+
foo: ['String']
520+
})
521+
})
522+
523+
test('generic from external-file', () => {
524+
const files = {
525+
'/foo.ts': 'export type P<T> = { foo: T }'
526+
}
527+
const { props } = resolve(
528+
`
529+
import { P } from './foo'
530+
defineProps<P<string>>()
531+
`,
532+
files
533+
)
534+
expect(props).toStrictEqual({
535+
foo: ['String']
536+
})
537+
})
538+
})
539+
458540
describe('external type imports', () => {
459541
test('relative ts', () => {
460542
const files = {

‎packages/compiler-sfc/src/script/resolveType.ts

+91-24
Original file line numberDiff line numberDiff line change
@@ -118,38 +118,46 @@ interface ResolvedElements {
118118
export function resolveTypeElements(
119119
ctx: TypeResolveContext,
120120
node: Node & MaybeWithScope & { _resolvedElements?: ResolvedElements },
121-
scope?: TypeScope
121+
scope?: TypeScope,
122+
typeParameters?: Record<string, Node>
122123
): ResolvedElements {
123124
if (node._resolvedElements) {
124125
return node._resolvedElements
125126
}
126127
return (node._resolvedElements = innerResolveTypeElements(
127128
ctx,
128129
node,
129-
node._ownerScope || scope || ctxToScope(ctx)
130+
node._ownerScope || scope || ctxToScope(ctx),
131+
typeParameters
130132
))
131133
}
132134

133135
function innerResolveTypeElements(
134136
ctx: TypeResolveContext,
135137
node: Node,
136-
scope: TypeScope
138+
scope: TypeScope,
139+
typeParameters?: Record<string, Node>
137140
): ResolvedElements {
138141
switch (node.type) {
139142
case 'TSTypeLiteral':
140-
return typeElementsToMap(ctx, node.members, scope)
143+
return typeElementsToMap(ctx, node.members, scope, typeParameters)
141144
case 'TSInterfaceDeclaration':
142-
return resolveInterfaceMembers(ctx, node, scope)
145+
return resolveInterfaceMembers(ctx, node, scope, typeParameters)
143146
case 'TSTypeAliasDeclaration':
144147
case 'TSParenthesizedType':
145-
return resolveTypeElements(ctx, node.typeAnnotation, scope)
148+
return resolveTypeElements(
149+
ctx,
150+
node.typeAnnotation,
151+
scope,
152+
typeParameters
153+
)
146154
case 'TSFunctionType': {
147155
return { props: {}, calls: [node] }
148156
}
149157
case 'TSUnionType':
150158
case 'TSIntersectionType':
151159
return mergeElements(
152-
node.types.map(t => resolveTypeElements(ctx, t, scope)),
160+
node.types.map(t => resolveTypeElements(ctx, t, scope, typeParameters)),
153161
node.type
154162
)
155163
case 'TSMappedType':
@@ -171,20 +179,57 @@ function innerResolveTypeElements(
171179
scope.imports[typeName]?.source === 'vue'
172180
) {
173181
return resolveExtractPropTypes(
174-
resolveTypeElements(ctx, node.typeParameters.params[0], scope),
182+
resolveTypeElements(
183+
ctx,
184+
node.typeParameters.params[0],
185+
scope,
186+
typeParameters
187+
),
175188
scope
176189
)
177190
}
178191
const resolved = resolveTypeReference(ctx, node, scope)
179192
if (resolved) {
180-
return resolveTypeElements(ctx, resolved, resolved._ownerScope)
193+
const typeParams: Record<string, Node> = Object.create(null)
194+
if (
195+
(resolved.type === 'TSTypeAliasDeclaration' ||
196+
resolved.type === 'TSInterfaceDeclaration') &&
197+
resolved.typeParameters &&
198+
node.typeParameters
199+
) {
200+
resolved.typeParameters.params.forEach((p, i) => {
201+
let param = typeParameters && typeParameters[p.name]
202+
if (!param) param = node.typeParameters!.params[i]
203+
typeParams[p.name] = param
204+
})
205+
}
206+
return resolveTypeElements(
207+
ctx,
208+
resolved,
209+
resolved._ownerScope,
210+
typeParams
211+
)
181212
} else {
182213
if (typeof typeName === 'string') {
214+
if (typeParameters && typeParameters[typeName]) {
215+
return resolveTypeElements(
216+
ctx,
217+
typeParameters[typeName],
218+
scope,
219+
typeParameters
220+
)
221+
}
183222
if (
184223
// @ts-ignore
185224
SupportedBuiltinsSet.has(typeName)
186225
) {
187-
return resolveBuiltin(ctx, node, typeName as any, scope)
226+
return resolveBuiltin(
227+
ctx,
228+
node,
229+
typeName as any,
230+
scope,
231+
typeParameters
232+
)
188233
} else if (typeName === 'ReturnType' && node.typeParameters) {
189234
// limited support, only reference types
190235
const ret = resolveReturnType(
@@ -243,11 +288,17 @@ function innerResolveTypeElements(
243288
function typeElementsToMap(
244289
ctx: TypeResolveContext,
245290
elements: TSTypeElement[],
246-
scope = ctxToScope(ctx)
291+
scope = ctxToScope(ctx),
292+
typeParameters?: Record<string, Node>
247293
): ResolvedElements {
248294
const res: ResolvedElements = { props: {} }
249295
for (const e of elements) {
250296
if (e.type === 'TSPropertySignature' || e.type === 'TSMethodSignature') {
297+
// capture generic parameters on node's scope
298+
if (typeParameters) {
299+
scope = createChildScope(scope)
300+
Object.assign(scope.types, typeParameters)
301+
}
251302
;(e as MaybeWithScope)._ownerScope = scope
252303
const name = getId(e.key)
253304
if (name && !e.computed) {
@@ -323,9 +374,15 @@ function createProperty(
323374
function resolveInterfaceMembers(
324375
ctx: TypeResolveContext,
325376
node: TSInterfaceDeclaration & MaybeWithScope,
326-
scope: TypeScope
377+
scope: TypeScope,
378+
typeParameters?: Record<string, Node>
327379
): ResolvedElements {
328-
const base = typeElementsToMap(ctx, node.body.body, node._ownerScope)
380+
const base = typeElementsToMap(
381+
ctx,
382+
node.body.body,
383+
node._ownerScope,
384+
typeParameters
385+
)
329386
if (node.extends) {
330387
for (const ext of node.extends) {
331388
if (
@@ -543,9 +600,15 @@ function resolveBuiltin(
543600
ctx: TypeResolveContext,
544601
node: TSTypeReference | TSExpressionWithTypeArguments,
545602
name: GetSetType<typeof SupportedBuiltinsSet>,
546-
scope: TypeScope
603+
scope: TypeScope,
604+
typeParameters?: Record<string, Node>
547605
): ResolvedElements {
548-
const t = resolveTypeElements(ctx, node.typeParameters!.params[0], scope)
606+
const t = resolveTypeElements(
607+
ctx,
608+
node.typeParameters!.params[0],
609+
scope,
610+
typeParameters
611+
)
549612
switch (name) {
550613
case 'Partial': {
551614
const res: ResolvedElements = { props: {}, calls: t.calls }
@@ -1103,14 +1166,7 @@ function moduleDeclToScope(
11031166
return node._resolvedChildScope
11041167
}
11051168

1106-
const scope = new TypeScope(
1107-
parentScope.filename,
1108-
parentScope.source,
1109-
parentScope.offset,
1110-
Object.create(parentScope.imports),
1111-
Object.create(parentScope.types),
1112-
Object.create(parentScope.declares)
1113-
)
1169+
const scope = createChildScope(parentScope)
11141170

11151171
if (node.body.type === 'TSModuleDeclaration') {
11161172
const decl = node.body as TSModuleDeclaration & WithScope
@@ -1124,6 +1180,17 @@ function moduleDeclToScope(
11241180
return (node._resolvedChildScope = scope)
11251181
}
11261182

1183+
function createChildScope(parentScope: TypeScope) {
1184+
return new TypeScope(
1185+
parentScope.filename,
1186+
parentScope.source,
1187+
parentScope.offset,
1188+
Object.create(parentScope.imports),
1189+
Object.create(parentScope.types),
1190+
Object.create(parentScope.declares)
1191+
)
1192+
}
1193+
11271194
const importExportRE = /^Import|^Export/
11281195

11291196
function recordTypes(
@@ -1262,7 +1329,7 @@ function recordType(
12621329
if (overwriteId || node.id) types[overwriteId || getId(node.id!)] = node
12631330
break
12641331
case 'TSTypeAliasDeclaration':
1265-
types[node.id.name] = node.typeAnnotation
1332+
types[node.id.name] = node.typeParameters ? node : node.typeAnnotation
12661333
break
12671334
case 'TSDeclareFunction':
12681335
if (node.id) declares[node.id.name] = node

0 commit comments

Comments
 (0)
Please sign in to comment.