Skip to content

Commit d1f973b

Browse files
committed
feat(compiler-sfc): support intersection and union types in macros
close #7553
1 parent a6dedc3 commit d1f973b

File tree

5 files changed

+269
-21
lines changed

5 files changed

+269
-21
lines changed

packages/compiler-sfc/__tests__/compileScript/__snapshots__/defineEmits.spec.ts.snap

+16
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,22 @@ export default /*#__PURE__*/_defineComponent({
191191
192192
193193
194+
return { emit }
195+
}
196+
197+
})"
198+
`;
199+
200+
exports[`defineEmits > w/ type (union) 1`] = `
201+
"import { defineComponent as _defineComponent } from 'vue'
202+
203+
export default /*#__PURE__*/_defineComponent({
204+
emits: [\\"foo\\", \\"bar\\", \\"baz\\"],
205+
setup(__props, { expose: __expose, emit }) {
206+
__expose();
207+
208+
209+
194210
return { emit }
195211
}
196212

packages/compiler-sfc/__tests__/compileScript/defineEmits.spec.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -47,13 +47,13 @@ const emit = defineEmits(['a', 'b'])
4747

4848
test('w/ type (union)', () => {
4949
const type = `((e: 'foo' | 'bar') => void) | ((e: 'baz', id: number) => void)`
50-
expect(() =>
51-
compile(`
50+
const { content } = compile(`
5251
<script setup lang="ts">
5352
const emit = defineEmits<${type}>()
5453
</script>
5554
`)
56-
).toThrow()
55+
assertCode(content)
56+
expect(content).toMatch(`emits: ["foo", "bar", "baz"]`)
5757
})
5858

5959
test('w/ type (type literal w/ call signatures)', () => {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
import { TSTypeAliasDeclaration } from '@babel/types'
2+
import { parse } from '../../src'
3+
import { ScriptCompileContext } from '../../src/script/context'
4+
import {
5+
inferRuntimeType,
6+
resolveTypeElements
7+
} from '../../src/script/resolveType'
8+
9+
describe('resolveType', () => {
10+
test('type literal', () => {
11+
const { elements, callSignatures } = resolve(`type Target = {
12+
foo: number // property
13+
bar(): void // method
14+
'baz': string // string literal key
15+
(e: 'foo'): void // call signature
16+
(e: 'bar'): void
17+
}`)
18+
expect(elements).toStrictEqual({
19+
foo: ['Number'],
20+
bar: ['Function'],
21+
baz: ['String']
22+
})
23+
expect(callSignatures?.length).toBe(2)
24+
})
25+
26+
test('reference type', () => {
27+
expect(
28+
resolve(`
29+
type Aliased = { foo: number }
30+
type Target = Aliased
31+
`).elements
32+
).toStrictEqual({
33+
foo: ['Number']
34+
})
35+
})
36+
37+
test('reference exported type', () => {
38+
expect(
39+
resolve(`
40+
export type Aliased = { foo: number }
41+
type Target = Aliased
42+
`).elements
43+
).toStrictEqual({
44+
foo: ['Number']
45+
})
46+
})
47+
48+
test('reference interface', () => {
49+
expect(
50+
resolve(`
51+
interface Aliased { foo: number }
52+
type Target = Aliased
53+
`).elements
54+
).toStrictEqual({
55+
foo: ['Number']
56+
})
57+
})
58+
59+
test('reference exported interface', () => {
60+
expect(
61+
resolve(`
62+
export interface Aliased { foo: number }
63+
type Target = Aliased
64+
`).elements
65+
).toStrictEqual({
66+
foo: ['Number']
67+
})
68+
})
69+
70+
test('reference interface extends', () => {
71+
expect(
72+
resolve(`
73+
export interface A { a(): void }
74+
export interface B extends A { b: boolean }
75+
interface C { c: string }
76+
interface Aliased extends B, C { foo: number }
77+
type Target = Aliased
78+
`).elements
79+
).toStrictEqual({
80+
a: ['Function'],
81+
b: ['Boolean'],
82+
c: ['String'],
83+
foo: ['Number']
84+
})
85+
})
86+
87+
test('function type', () => {
88+
expect(
89+
resolve(`
90+
type Target = (e: 'foo') => void
91+
`).callSignatures?.length
92+
).toBe(1)
93+
})
94+
95+
test('reference function type', () => {
96+
expect(
97+
resolve(`
98+
type Fn = (e: 'foo') => void
99+
type Target = Fn
100+
`).callSignatures?.length
101+
).toBe(1)
102+
})
103+
104+
test('intersection type', () => {
105+
expect(
106+
resolve(`
107+
type Foo = { foo: number }
108+
type Bar = { bar: string }
109+
type Baz = { bar: string | boolean }
110+
type Target = { self: any } & Foo & Bar & Baz
111+
`).elements
112+
).toStrictEqual({
113+
self: ['Unknown'],
114+
foo: ['Number'],
115+
// both Bar & Baz has 'bar', but Baz['bar] is wider so it should be
116+
// preferred
117+
bar: ['String', 'Boolean']
118+
})
119+
})
120+
121+
// #7553
122+
test('union type', () => {
123+
expect(
124+
resolve(`
125+
interface CommonProps {
126+
size?: 'xl' | 'l' | 'm' | 's' | 'xs'
127+
}
128+
129+
type ConditionalProps =
130+
| {
131+
color: 'normal' | 'primary' | 'secondary'
132+
appearance: 'normal' | 'outline' | 'text'
133+
}
134+
| {
135+
color: number
136+
appearance: 'outline'
137+
note: string
138+
}
139+
140+
type Target = CommonProps & ConditionalProps
141+
`).elements
142+
).toStrictEqual({
143+
size: ['String'],
144+
color: ['String', 'Number'],
145+
appearance: ['String'],
146+
note: ['String']
147+
})
148+
})
149+
150+
// describe('built-in utility types', () => {
151+
152+
// })
153+
154+
describe('errors', () => {
155+
test('error on computed keys', () => {
156+
expect(() => resolve(`type Target = { [Foo]: string }`)).toThrow(
157+
`computed keys are not supported in types referenced by SFC macros`
158+
)
159+
})
160+
})
161+
})
162+
163+
function resolve(code: string) {
164+
const { descriptor } = parse(`<script setup lang="ts">${code}</script>`)
165+
const ctx = new ScriptCompileContext(descriptor, { id: 'test' })
166+
const targetDecl = ctx.scriptSetupAst!.body.find(
167+
s => s.type === 'TSTypeAliasDeclaration' && s.id.name === 'Target'
168+
) as TSTypeAliasDeclaration
169+
const raw = resolveTypeElements(ctx, targetDecl.typeAnnotation)
170+
const elements: Record<string, string[]> = {}
171+
for (const key in raw) {
172+
elements[key] = inferRuntimeType(ctx, raw[key])
173+
}
174+
return {
175+
elements,
176+
callSignatures: raw.__callSignatures,
177+
raw
178+
}
179+
}

packages/compiler-sfc/src/script/defineProps.ts

+8-13
Original file line numberDiff line numberDiff line change
@@ -193,20 +193,15 @@ function resolveRuntimePropsFromType(
193193
const elements = resolveTypeElements(ctx, node)
194194
for (const key in elements) {
195195
const e = elements[key]
196-
let type: string[] | undefined
196+
let type = inferRuntimeType(ctx, e)
197197
let skipCheck = false
198-
if (e.type === 'TSMethodSignature') {
199-
type = ['Function']
200-
} else if (e.typeAnnotation) {
201-
type = inferRuntimeType(ctx, e.typeAnnotation.typeAnnotation)
202-
// skip check for result containing unknown types
203-
if (type.includes(UNKNOWN_TYPE)) {
204-
if (type.includes('Boolean') || type.includes('Function')) {
205-
type = type.filter(t => t !== UNKNOWN_TYPE)
206-
skipCheck = true
207-
} else {
208-
type = ['null']
209-
}
198+
// skip check for result containing unknown types
199+
if (type.includes(UNKNOWN_TYPE)) {
200+
if (type.includes('Boolean') || type.includes('Function')) {
201+
type = type.filter(t => t !== UNKNOWN_TYPE)
202+
skipCheck = true
203+
} else {
204+
type = ['null']
210205
}
211206
}
212207
props.push({

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

+63-5
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ import { UNKNOWN_TYPE } from './utils'
1616
import { ScriptCompileContext } from './context'
1717
import { ImportBinding } from '../compileScript'
1818
import { TSInterfaceDeclaration } from '@babel/types'
19-
import { hasOwn } from '@vue/shared'
19+
import { hasOwn, isArray } from '@vue/shared'
20+
import { Expression } from '@babel/types'
2021

2122
export interface TypeScope {
2223
filename: string
@@ -63,24 +64,37 @@ function innerResolveTypeElements(
6364
addCallSignature(ret, node)
6465
return ret
6566
}
66-
case 'TSExpressionWithTypeArguments':
67+
case 'TSExpressionWithTypeArguments': // referenced by interface extends
6768
case 'TSTypeReference':
6869
return resolveTypeElements(ctx, resolveTypeReference(ctx, node))
70+
case 'TSUnionType':
71+
case 'TSIntersectionType':
72+
return mergeElements(
73+
node.types.map(t => resolveTypeElements(ctx, t)),
74+
node.type
75+
)
6976
}
7077
ctx.error(`Unsupported type in SFC macro: ${node.type}`, node)
7178
}
7279

7380
function addCallSignature(
7481
elements: ResolvedElements,
75-
node: TSCallSignatureDeclaration | TSFunctionType
82+
node:
83+
| TSCallSignatureDeclaration
84+
| TSFunctionType
85+
| (TSCallSignatureDeclaration | TSFunctionType)[]
7686
) {
7787
if (!elements.__callSignatures) {
7888
Object.defineProperty(elements, '__callSignatures', {
7989
enumerable: false,
80-
value: [node]
90+
value: isArray(node) ? node : [node]
8191
})
8292
} else {
83-
elements.__callSignatures.push(node)
93+
if (isArray(node)) {
94+
elements.__callSignatures.push(...node)
95+
} else {
96+
elements.__callSignatures.push(node)
97+
}
8498
}
8599
}
86100

@@ -112,6 +126,45 @@ function typeElementsToMap(
112126
return ret
113127
}
114128

129+
function mergeElements(
130+
maps: ResolvedElements[],
131+
type: 'TSUnionType' | 'TSIntersectionType'
132+
): ResolvedElements {
133+
const res: ResolvedElements = Object.create(null)
134+
for (const m of maps) {
135+
for (const key in m) {
136+
if (!(key in res)) {
137+
res[key] = m[key]
138+
} else {
139+
res[key] = createProperty(res[key].key, type, [res[key], m[key]])
140+
}
141+
}
142+
if (m.__callSignatures) {
143+
addCallSignature(res, m.__callSignatures)
144+
}
145+
}
146+
return res
147+
}
148+
149+
function createProperty(
150+
key: Expression,
151+
type: 'TSUnionType' | 'TSIntersectionType',
152+
types: Node[]
153+
): TSPropertySignature {
154+
return {
155+
type: 'TSPropertySignature',
156+
key,
157+
kind: 'get',
158+
typeAnnotation: {
159+
type: 'TSTypeAnnotation',
160+
typeAnnotation: {
161+
type,
162+
types: types as TSType[]
163+
}
164+
}
165+
}
166+
}
167+
115168
function resolveInterfaceMembers(
116169
ctx: ScriptCompileContext,
117170
node: TSInterfaceDeclaration
@@ -252,6 +305,11 @@ export function inferRuntimeType(
252305
}
253306
return types.size ? Array.from(types) : ['Object']
254307
}
308+
case 'TSPropertySignature':
309+
if (node.typeAnnotation) {
310+
return inferRuntimeType(ctx, node.typeAnnotation.typeAnnotation)
311+
}
312+
case 'TSMethodSignature':
255313
case 'TSFunctionType':
256314
return ['Function']
257315
case 'TSArrayType':

0 commit comments

Comments
 (0)