Skip to content

Commit

Permalink
fix(compiler-sfc): support inferring generic types (#8511)
Browse files Browse the repository at this point in the history
close #8482
  • Loading branch information
edison1105 authored Dec 1, 2023
1 parent 6345197 commit eb5e307
Show file tree
Hide file tree
Showing 2 changed files with 173 additions and 24 deletions.
82 changes: 82 additions & 0 deletions packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,88 @@ describe('resolveType', () => {
})
})

describe('generics', () => {
test('generic with type literal', () => {
expect(
resolve(`
type Props<T> = T
defineProps<Props<{ foo: string }>>()
`).props
).toStrictEqual({
foo: ['String']
})
})

test('generic used in intersection', () => {
expect(
resolve(`
type Foo = { foo: string; }
type Bar = { bar: number; }
type Props<T,U> = T & U & { baz: boolean }
defineProps<Props<Foo, Bar>>()
`).props
).toStrictEqual({
foo: ['String'],
bar: ['Number'],
baz: ['Boolean']
})
})

test('generic type /w generic type alias', () => {
expect(
resolve(`
type Aliased<T> = Readonly<Partial<T>>
type Props<T> = Aliased<T>
type Foo = { foo: string; }
defineProps<Props<Foo>>()
`).props
).toStrictEqual({
foo: ['String']
})
})

test('generic type /w aliased type literal', () => {
expect(
resolve(`
type Aliased<T> = { foo: T }
defineProps<Aliased<string>>()
`).props
).toStrictEqual({
foo: ['String']
})
})

test('generic type /w interface', () => {
expect(
resolve(`
interface Props<T> {
foo: T
}
type Foo = string
defineProps<Props<Foo>>()
`).props
).toStrictEqual({
foo: ['String']
})
})

test('generic from external-file', () => {
const files = {
'/foo.ts': 'export type P<T> = { foo: T }'
}
const { props } = resolve(
`
import { P } from './foo'
defineProps<P<string>>()
`,
files
)
expect(props).toStrictEqual({
foo: ['String']
})
})
})

describe('external type imports', () => {
test('relative ts', () => {
const files = {
Expand Down
115 changes: 91 additions & 24 deletions packages/compiler-sfc/src/script/resolveType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,38 +118,46 @@ interface ResolvedElements {
export function resolveTypeElements(
ctx: TypeResolveContext,
node: Node & MaybeWithScope & { _resolvedElements?: ResolvedElements },
scope?: TypeScope
scope?: TypeScope,
typeParameters?: Record<string, Node>
): ResolvedElements {
if (node._resolvedElements) {
return node._resolvedElements
}
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<string, Node>
): 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':
Expand All @@ -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<string, Node> = 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(
Expand Down Expand Up @@ -243,11 +288,17 @@ function innerResolveTypeElements(
function typeElementsToMap(
ctx: TypeResolveContext,
elements: TSTypeElement[],
scope = ctxToScope(ctx)
scope = ctxToScope(ctx),
typeParameters?: Record<string, Node>
): 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) {
Expand Down Expand Up @@ -323,9 +374,15 @@ function createProperty(
function resolveInterfaceMembers(
ctx: TypeResolveContext,
node: TSInterfaceDeclaration & MaybeWithScope,
scope: TypeScope
scope: TypeScope,
typeParameters?: Record<string, Node>
): 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 (
Expand Down Expand Up @@ -543,9 +600,15 @@ function resolveBuiltin(
ctx: TypeResolveContext,
node: TSTypeReference | TSExpressionWithTypeArguments,
name: GetSetType<typeof SupportedBuiltinsSet>,
scope: TypeScope
scope: TypeScope,
typeParameters?: Record<string, Node>
): 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 }
Expand Down Expand Up @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit eb5e307

Please sign in to comment.