Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(compiler-sfc): support infer generic type #8511

Merged
merged 6 commits into from
Dec 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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