Skip to content

Commit eae96dc

Browse files
authored
feat: union props (#1)
* fix(runtime-core): withDefaults & union types * feat: union props * chore: add distributive union test * chore: improve tests
1 parent aa9ef23 commit eae96dc

File tree

8 files changed

+398
-15
lines changed

8 files changed

+398
-15
lines changed

packages-private/dts-test/setupHelpers.test-d.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,99 @@ describe('defineProps w/ generic type declaration + withDefaults', <T extends
207207
expectType<boolean>(res.bool)
208208
})
209209

210+
describe('defineProps w/union type', () => {
211+
type PP =
212+
| {
213+
type: 'text'
214+
mm: string
215+
}
216+
| {
217+
type: 'number'
218+
mm: number | null
219+
}
220+
221+
const res = defineProps<PP>()
222+
expectType<string | number | null>(res.mm)
223+
224+
if (res.type === 'text') {
225+
expectType<string>(res.mm)
226+
}
227+
228+
if (res.type === 'number') {
229+
expectType<number | null>(res.mm)
230+
}
231+
})
232+
233+
describe('defineProps w/distributive union type', () => {
234+
type PP =
235+
| {
236+
type1: 'text'
237+
mm1: string
238+
}
239+
| {
240+
type2: 'number'
241+
mm2: number | null
242+
}
243+
244+
const res = defineProps<PP>()
245+
246+
if ('type1' in res) {
247+
expectType<string>(res.mm1)
248+
}
249+
250+
if ('type2' in res) {
251+
expectType<number | null>(res.mm2)
252+
}
253+
})
254+
255+
describe('withDefaults w/ union type', () => {
256+
type PP =
257+
| {
258+
type?: 'text'
259+
mm: string
260+
}
261+
| {
262+
type?: 'number'
263+
mm: number | null
264+
}
265+
266+
const res = withDefaults(defineProps<PP>(), {
267+
type: 'text',
268+
})
269+
270+
if (res.type && res.type === 'text') {
271+
expectType<string>(res.mm)
272+
}
273+
274+
if (res.type === 'number') {
275+
expectType<number | null>(res.mm)
276+
}
277+
})
278+
279+
describe('withDefaults w/ generic union type', <T extends
280+
| string
281+
| number>() => {
282+
type PP =
283+
| {
284+
tt?: 'a'
285+
mm: T
286+
}
287+
| {
288+
tt?: 'b'
289+
mm: T[]
290+
}
291+
292+
const res = withDefaults(defineProps<PP>(), {
293+
tt: 'a',
294+
})
295+
296+
if (res.tt === 'a') {
297+
expectType<T>(res.mm)
298+
} else {
299+
expectType<T[]>(res.mm)
300+
}
301+
})
302+
210303
describe('withDefaults w/ boolean type', () => {
211304
const res1 = withDefaults(
212305
defineProps<{

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

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,68 @@ return { foo }
9393
})"
9494
`;
9595

96+
exports[`defineProps > discriminated union 1`] = `
97+
"import { defineComponent as _defineComponent } from 'vue'
98+
99+
export default /*@__PURE__*/_defineComponent({
100+
props: {
101+
tag: { type: [String, Number], required: true, union: [['d1'], ['d2']] },
102+
d1: { type: Number, required: true, union: ['tag'] },
103+
d2: { type: String, required: true, union: ['tag'] }
104+
},
105+
setup(__props: any, { expose: __expose }) {
106+
__expose();
107+
108+
const props = __props;
109+
110+
return { props }
111+
}
112+
113+
})"
114+
`;
115+
116+
exports[`defineProps > distributive union w/ conditional keys 1`] = `
117+
"import { defineComponent as _defineComponent } from 'vue'
118+
119+
export default /*@__PURE__*/_defineComponent({
120+
props: {
121+
a: { type: String, required: false, union: ['b'] },
122+
b: { type: Number, required: true, union: ['a'] },
123+
c: { type: Number, required: true, union: ['d'] },
124+
d: { type: String, required: false, union: ['c'] }
125+
},
126+
setup(__props: any, { expose: __expose }) {
127+
__expose();
128+
129+
const props = __props;
130+
131+
return { props }
132+
}
133+
134+
})"
135+
`;
136+
137+
exports[`defineProps > distributive union with boolean keys 1`] = `
138+
"import { defineComponent as _defineComponent } from 'vue'
139+
140+
export default /*@__PURE__*/_defineComponent({
141+
props: {
142+
a: { type: String, required: true, union: ['b'] },
143+
b: { type: Number, required: true, union: ['a'] },
144+
c: { type: Boolean, required: true, union: ['d'] },
145+
d: { type: String, required: true, union: ['c'] }
146+
},
147+
setup(__props: any, { expose: __expose }) {
148+
__expose();
149+
150+
const props = __props;
151+
152+
return { props }
153+
}
154+
155+
})"
156+
`;
157+
96158
exports[`defineProps > should escape names w/ special symbols 1`] = `
97159
"import { defineComponent as _defineComponent } from 'vue'
98160

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

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -399,6 +399,54 @@ const props = defineProps({ foo: String })
399399
assertCode(content)
400400
})
401401

402+
test('distributive union w/ conditional keys', () => {
403+
const { content } = compile(`
404+
<script setup lang="ts">
405+
const props = withDefaults(defineProps<{
406+
a?: string;
407+
b: number;
408+
} | {
409+
c: number;
410+
d?: string;
411+
}>(), {
412+
});
413+
</script>
414+
`)
415+
assertCode(content)
416+
})
417+
418+
test('discriminated union', () => {
419+
const { content } = compile(`
420+
<script setup lang="ts">
421+
const props = withDefaults(defineProps<{
422+
tag: string;
423+
d1: number;
424+
} | {
425+
tag: number;
426+
d2: string;
427+
}>(), {
428+
});
429+
</script>
430+
`)
431+
assertCode(content)
432+
})
433+
434+
test('distributive union with boolean keys', () => {
435+
const { content } = compile(`
436+
<script setup lang="ts">
437+
const props = withDefaults(defineProps<{
438+
a: string;
439+
b: number;
440+
} | {
441+
c: boolean;
442+
d: string;
443+
}>(), {
444+
});
445+
</script>
446+
`)
447+
assertCode(content)
448+
})
449+
402450
// #7111
403451
test('withDefaults (static) w/ production mode', () => {
404452
const { content } = compile(

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

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
import { genModelProps } from './defineModel'
2626
import { getObjectOrArrayExpressionKeys } from './analyzeScriptBindings'
2727
import { processPropsDestructure } from './definePropsDestructure'
28+
import { isArray } from '@vue/shared'
2829

2930
export const DEFINE_PROPS = 'defineProps'
3031
export const WITH_DEFAULTS = 'withDefaults'
@@ -34,8 +35,11 @@ export interface PropTypeData {
3435
type: string[]
3536
required: boolean
3637
skipCheck: boolean
38+
union?: UnionDefinition
3739
}
3840

41+
export type UnionDefinition = string[] | string[][]
42+
3943
export type PropsDestructureBindings = Record<
4044
string, // public prop key
4145
{
@@ -231,6 +235,7 @@ function resolveRuntimePropsFromType(
231235
key,
232236
required: !e.optional,
233237
type: type || [`null`],
238+
union: e.union,
234239
skipCheck,
235240
})
236241
}
@@ -239,7 +244,7 @@ function resolveRuntimePropsFromType(
239244

240245
function genRuntimePropFromType(
241246
ctx: TypeResolveContext,
242-
{ key, required, type, skipCheck }: PropTypeData,
247+
{ key, required, type, skipCheck, union }: PropTypeData,
243248
hasStaticDefaults: boolean,
244249
): string {
245250
let defaultString: string | undefined
@@ -272,6 +277,7 @@ function genRuntimePropFromType(
272277
return `${finalKey}: { ${concatStrings([
273278
`type: ${toRuntimeTypeString(type)}`,
274279
`required: ${required}`,
280+
union && `union: ${genUnionArrayString(union)}`,
275281
skipCheck && 'skipCheck: true',
276282
defaultString,
277283
])} }`
@@ -306,6 +312,18 @@ function genRuntimePropFromType(
306312
}
307313
}
308314

315+
function genUnionArrayString(union: UnionDefinition): string {
316+
const entries = union.map(key => {
317+
if (isArray(key)) {
318+
return genUnionArrayString(key)
319+
} else {
320+
return `'${key}'`
321+
}
322+
})
323+
324+
return `[${entries.join(', ')}]`
325+
}
326+
309327
/**
310328
* check defaults. If the default object is an object literal with only
311329
* static properties, we can directly generate more optimized default

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

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import type TS from 'typescript'
4242
import { dirname, extname, join } from 'path'
4343
import { minimatch as isMatch } from 'minimatch'
4444
import * as process from 'process'
45+
import type { UnionDefinition } from './defineProps'
4546

4647
export type SimpleTypeResolveOptions = Partial<
4748
Pick<
@@ -125,14 +126,16 @@ export interface MaybeWithScope {
125126
_ownerScope?: TypeScope
126127
}
127128

128-
interface ResolvedElements {
129-
props: Record<
130-
string,
131-
(TSPropertySignature | TSMethodSignature) & {
132-
// resolved props always has ownerScope attached
133-
_ownerScope: TypeScope
134-
}
135-
>
129+
export interface MaybeWithUnion {
130+
union?: UnionDefinition
131+
}
132+
133+
export type ResolvedElementProp = (TSPropertySignature | TSMethodSignature) &
134+
WithScope &
135+
MaybeWithUnion
136+
137+
export interface ResolvedElements {
138+
props: Record<string, ResolvedElementProp>
136139
calls?: (TSCallSignatureDeclaration | TSFunctionType)[]
137140
}
138141

@@ -368,6 +371,9 @@ function mergeElements(
368371
for (const key in props) {
369372
if (!hasOwn(baseProps, key)) {
370373
baseProps[key] = props[key]
374+
375+
const keys = Object.keys(props).filter(k => k !== key)
376+
baseProps[key].union = keys
371377
} else {
372378
baseProps[key] = createProperty(
373379
baseProps[key].key,
@@ -378,6 +384,10 @@ function mergeElements(
378384
},
379385
baseProps[key]._ownerScope,
380386
baseProps[key].optional || props[key].optional,
387+
[
388+
baseProps[key].union || [],
389+
Object.keys(props).filter(k => k !== key),
390+
],
381391
)
382392
}
383393
}
@@ -393,7 +403,8 @@ function createProperty(
393403
typeAnnotation: TSType,
394404
scope: TypeScope,
395405
optional: boolean,
396-
): TSPropertySignature & WithScope {
406+
union?: UnionDefinition,
407+
): TSPropertySignature & WithScope & MaybeWithUnion {
397408
return {
398409
type: 'TSPropertySignature',
399410
key,
@@ -404,6 +415,7 @@ function createProperty(
404415
typeAnnotation,
405416
},
406417
_ownerScope: scope,
418+
union,
407419
}
408420
}
409421

0 commit comments

Comments
 (0)