diff --git a/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineEmits.spec.ts.snap b/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineEmits.spec.ts.snap
index 5add78a28b3..729c019a555 100644
--- a/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineEmits.spec.ts.snap
+++ b/packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineEmits.spec.ts.snap
@@ -191,6 +191,22 @@ export default /*#__PURE__*/_defineComponent({
+return { emit }
+}
+
+})"
+`;
+
+exports[`defineEmits > w/ type (union) 1`] = `
+"import { defineComponent as _defineComponent } from 'vue'
+
+export default /*#__PURE__*/_defineComponent({
+ emits: [\\"foo\\", \\"bar\\", \\"baz\\"],
+ setup(__props, { expose: __expose, emit }) {
+ __expose();
+
+
+
return { emit }
}
diff --git a/packages/compiler-sfc/__tests__/compileScript/defineEmits.spec.ts b/packages/compiler-sfc/__tests__/compileScript/defineEmits.spec.ts
index 3920f08efb8..67d9674b54c 100644
--- a/packages/compiler-sfc/__tests__/compileScript/defineEmits.spec.ts
+++ b/packages/compiler-sfc/__tests__/compileScript/defineEmits.spec.ts
@@ -47,13 +47,13 @@ const emit = defineEmits(['a', 'b'])
test('w/ type (union)', () => {
const type = `((e: 'foo' | 'bar') => void) | ((e: 'baz', id: number) => void)`
- expect(() =>
- compile(`
+ const { content } = compile(`
`)
- ).toThrow()
+ assertCode(content)
+ expect(content).toMatch(`emits: ["foo", "bar", "baz"]`)
})
test('w/ type (type literal w/ call signatures)', () => {
diff --git a/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts
new file mode 100644
index 00000000000..12d18e40687
--- /dev/null
+++ b/packages/compiler-sfc/__tests__/compileScript/resolveType.spec.ts
@@ -0,0 +1,179 @@
+import { TSTypeAliasDeclaration } from '@babel/types'
+import { parse } from '../../src'
+import { ScriptCompileContext } from '../../src/script/context'
+import {
+ inferRuntimeType,
+ resolveTypeElements
+} from '../../src/script/resolveType'
+
+describe('resolveType', () => {
+ test('type literal', () => {
+ const { elements, callSignatures } = resolve(`type Target = {
+ foo: number // property
+ bar(): void // method
+ 'baz': string // string literal key
+ (e: 'foo'): void // call signature
+ (e: 'bar'): void
+ }`)
+ expect(elements).toStrictEqual({
+ foo: ['Number'],
+ bar: ['Function'],
+ baz: ['String']
+ })
+ expect(callSignatures?.length).toBe(2)
+ })
+
+ test('reference type', () => {
+ expect(
+ resolve(`
+ type Aliased = { foo: number }
+ type Target = Aliased
+ `).elements
+ ).toStrictEqual({
+ foo: ['Number']
+ })
+ })
+
+ test('reference exported type', () => {
+ expect(
+ resolve(`
+ export type Aliased = { foo: number }
+ type Target = Aliased
+ `).elements
+ ).toStrictEqual({
+ foo: ['Number']
+ })
+ })
+
+ test('reference interface', () => {
+ expect(
+ resolve(`
+ interface Aliased { foo: number }
+ type Target = Aliased
+ `).elements
+ ).toStrictEqual({
+ foo: ['Number']
+ })
+ })
+
+ test('reference exported interface', () => {
+ expect(
+ resolve(`
+ export interface Aliased { foo: number }
+ type Target = Aliased
+ `).elements
+ ).toStrictEqual({
+ foo: ['Number']
+ })
+ })
+
+ test('reference interface extends', () => {
+ expect(
+ resolve(`
+ export interface A { a(): void }
+ export interface B extends A { b: boolean }
+ interface C { c: string }
+ interface Aliased extends B, C { foo: number }
+ type Target = Aliased
+ `).elements
+ ).toStrictEqual({
+ a: ['Function'],
+ b: ['Boolean'],
+ c: ['String'],
+ foo: ['Number']
+ })
+ })
+
+ test('function type', () => {
+ expect(
+ resolve(`
+ type Target = (e: 'foo') => void
+ `).callSignatures?.length
+ ).toBe(1)
+ })
+
+ test('reference function type', () => {
+ expect(
+ resolve(`
+ type Fn = (e: 'foo') => void
+ type Target = Fn
+ `).callSignatures?.length
+ ).toBe(1)
+ })
+
+ test('intersection type', () => {
+ expect(
+ resolve(`
+ type Foo = { foo: number }
+ type Bar = { bar: string }
+ type Baz = { bar: string | boolean }
+ type Target = { self: any } & Foo & Bar & Baz
+ `).elements
+ ).toStrictEqual({
+ self: ['Unknown'],
+ foo: ['Number'],
+ // both Bar & Baz has 'bar', but Baz['bar] is wider so it should be
+ // preferred
+ bar: ['String', 'Boolean']
+ })
+ })
+
+ // #7553
+ test('union type', () => {
+ expect(
+ resolve(`
+ interface CommonProps {
+ size?: 'xl' | 'l' | 'm' | 's' | 'xs'
+ }
+
+ type ConditionalProps =
+ | {
+ color: 'normal' | 'primary' | 'secondary'
+ appearance: 'normal' | 'outline' | 'text'
+ }
+ | {
+ color: number
+ appearance: 'outline'
+ note: string
+ }
+
+ type Target = CommonProps & ConditionalProps
+ `).elements
+ ).toStrictEqual({
+ size: ['String'],
+ color: ['String', 'Number'],
+ appearance: ['String'],
+ note: ['String']
+ })
+ })
+
+ // describe('built-in utility types', () => {
+
+ // })
+
+ describe('errors', () => {
+ test('error on computed keys', () => {
+ expect(() => resolve(`type Target = { [Foo]: string }`)).toThrow(
+ `computed keys are not supported in types referenced by SFC macros`
+ )
+ })
+ })
+})
+
+function resolve(code: string) {
+ const { descriptor } = parse(``)
+ const ctx = new ScriptCompileContext(descriptor, { id: 'test' })
+ const targetDecl = ctx.scriptSetupAst!.body.find(
+ s => s.type === 'TSTypeAliasDeclaration' && s.id.name === 'Target'
+ ) as TSTypeAliasDeclaration
+ const raw = resolveTypeElements(ctx, targetDecl.typeAnnotation)
+ const elements: Record = {}
+ for (const key in raw) {
+ elements[key] = inferRuntimeType(ctx, raw[key])
+ }
+ return {
+ elements,
+ callSignatures: raw.__callSignatures,
+ raw
+ }
+}
diff --git a/packages/compiler-sfc/src/script/defineProps.ts b/packages/compiler-sfc/src/script/defineProps.ts
index bd462a2a8ea..ee8b5e55734 100644
--- a/packages/compiler-sfc/src/script/defineProps.ts
+++ b/packages/compiler-sfc/src/script/defineProps.ts
@@ -193,20 +193,15 @@ function resolveRuntimePropsFromType(
const elements = resolveTypeElements(ctx, node)
for (const key in elements) {
const e = elements[key]
- let type: string[] | undefined
+ let type = inferRuntimeType(ctx, e)
let skipCheck = false
- if (e.type === 'TSMethodSignature') {
- type = ['Function']
- } else if (e.typeAnnotation) {
- type = inferRuntimeType(ctx, e.typeAnnotation.typeAnnotation)
- // skip check for result containing unknown types
- if (type.includes(UNKNOWN_TYPE)) {
- if (type.includes('Boolean') || type.includes('Function')) {
- type = type.filter(t => t !== UNKNOWN_TYPE)
- skipCheck = true
- } else {
- type = ['null']
- }
+ // skip check for result containing unknown types
+ if (type.includes(UNKNOWN_TYPE)) {
+ if (type.includes('Boolean') || type.includes('Function')) {
+ type = type.filter(t => t !== UNKNOWN_TYPE)
+ skipCheck = true
+ } else {
+ type = ['null']
}
}
props.push({
diff --git a/packages/compiler-sfc/src/script/resolveType.ts b/packages/compiler-sfc/src/script/resolveType.ts
index ba41757069e..6711784a7af 100644
--- a/packages/compiler-sfc/src/script/resolveType.ts
+++ b/packages/compiler-sfc/src/script/resolveType.ts
@@ -16,7 +16,8 @@ import { UNKNOWN_TYPE } from './utils'
import { ScriptCompileContext } from './context'
import { ImportBinding } from '../compileScript'
import { TSInterfaceDeclaration } from '@babel/types'
-import { hasOwn } from '@vue/shared'
+import { hasOwn, isArray } from '@vue/shared'
+import { Expression } from '@babel/types'
export interface TypeScope {
filename: string
@@ -63,24 +64,37 @@ function innerResolveTypeElements(
addCallSignature(ret, node)
return ret
}
- case 'TSExpressionWithTypeArguments':
+ case 'TSExpressionWithTypeArguments': // referenced by interface extends
case 'TSTypeReference':
return resolveTypeElements(ctx, resolveTypeReference(ctx, node))
+ case 'TSUnionType':
+ case 'TSIntersectionType':
+ return mergeElements(
+ node.types.map(t => resolveTypeElements(ctx, t)),
+ node.type
+ )
}
ctx.error(`Unsupported type in SFC macro: ${node.type}`, node)
}
function addCallSignature(
elements: ResolvedElements,
- node: TSCallSignatureDeclaration | TSFunctionType
+ node:
+ | TSCallSignatureDeclaration
+ | TSFunctionType
+ | (TSCallSignatureDeclaration | TSFunctionType)[]
) {
if (!elements.__callSignatures) {
Object.defineProperty(elements, '__callSignatures', {
enumerable: false,
- value: [node]
+ value: isArray(node) ? node : [node]
})
} else {
- elements.__callSignatures.push(node)
+ if (isArray(node)) {
+ elements.__callSignatures.push(...node)
+ } else {
+ elements.__callSignatures.push(node)
+ }
}
}
@@ -112,6 +126,45 @@ function typeElementsToMap(
return ret
}
+function mergeElements(
+ maps: ResolvedElements[],
+ type: 'TSUnionType' | 'TSIntersectionType'
+): ResolvedElements {
+ const res: ResolvedElements = Object.create(null)
+ for (const m of maps) {
+ for (const key in m) {
+ if (!(key in res)) {
+ res[key] = m[key]
+ } else {
+ res[key] = createProperty(res[key].key, type, [res[key], m[key]])
+ }
+ }
+ if (m.__callSignatures) {
+ addCallSignature(res, m.__callSignatures)
+ }
+ }
+ return res
+}
+
+function createProperty(
+ key: Expression,
+ type: 'TSUnionType' | 'TSIntersectionType',
+ types: Node[]
+): TSPropertySignature {
+ return {
+ type: 'TSPropertySignature',
+ key,
+ kind: 'get',
+ typeAnnotation: {
+ type: 'TSTypeAnnotation',
+ typeAnnotation: {
+ type,
+ types: types as TSType[]
+ }
+ }
+ }
+}
+
function resolveInterfaceMembers(
ctx: ScriptCompileContext,
node: TSInterfaceDeclaration
@@ -252,6 +305,11 @@ export function inferRuntimeType(
}
return types.size ? Array.from(types) : ['Object']
}
+ case 'TSPropertySignature':
+ if (node.typeAnnotation) {
+ return inferRuntimeType(ctx, node.typeAnnotation.typeAnnotation)
+ }
+ case 'TSMethodSignature':
case 'TSFunctionType':
return ['Function']
case 'TSArrayType':