Skip to content

Commit

Permalink
feat: union props (#1)
Browse files Browse the repository at this point in the history
* fix(runtime-core): withDefaults & union types

* feat: union props

* chore: add distributive union test

* chore: improve tests
  • Loading branch information
davidmatter committed Sep 27, 2024
1 parent aa9ef23 commit eae96dc
Show file tree
Hide file tree
Showing 8 changed files with 398 additions and 15 deletions.
93 changes: 93 additions & 0 deletions packages-private/dts-test/setupHelpers.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,99 @@ describe('defineProps w/ generic type declaration + withDefaults', <T extends
expectType<boolean>(res.bool)
})

describe('defineProps w/union type', () => {
type PP =
| {
type: 'text'
mm: string
}
| {
type: 'number'
mm: number | null
}

const res = defineProps<PP>()
expectType<string | number | null>(res.mm)

if (res.type === 'text') {
expectType<string>(res.mm)
}

if (res.type === 'number') {
expectType<number | null>(res.mm)
}
})

describe('defineProps w/distributive union type', () => {
type PP =
| {
type1: 'text'
mm1: string
}
| {
type2: 'number'
mm2: number | null
}

const res = defineProps<PP>()

if ('type1' in res) {
expectType<string>(res.mm1)
}

if ('type2' in res) {
expectType<number | null>(res.mm2)
}
})

describe('withDefaults w/ union type', () => {
type PP =
| {
type?: 'text'
mm: string
}
| {
type?: 'number'
mm: number | null
}

const res = withDefaults(defineProps<PP>(), {
type: 'text',
})

if (res.type && res.type === 'text') {
expectType<string>(res.mm)
}

if (res.type === 'number') {
expectType<number | null>(res.mm)
}
})

describe('withDefaults w/ generic union type', <T extends
| string
| number>() => {
type PP =
| {
tt?: 'a'
mm: T
}
| {
tt?: 'b'
mm: T[]
}

const res = withDefaults(defineProps<PP>(), {
tt: 'a',
})

if (res.tt === 'a') {
expectType<T>(res.mm)
} else {
expectType<T[]>(res.mm)
}
})

describe('withDefaults w/ boolean type', () => {
const res1 = withDefaults(
defineProps<{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,68 @@ return { foo }
})"
`;

exports[`defineProps > discriminated union 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
export default /*@__PURE__*/_defineComponent({
props: {
tag: { type: [String, Number], required: true, union: [['d1'], ['d2']] },
d1: { type: Number, required: true, union: ['tag'] },
d2: { type: String, required: true, union: ['tag'] }
},
setup(__props: any, { expose: __expose }) {
__expose();
const props = __props;
return { props }
}
})"
`;

exports[`defineProps > distributive union w/ conditional keys 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
export default /*@__PURE__*/_defineComponent({
props: {
a: { type: String, required: false, union: ['b'] },
b: { type: Number, required: true, union: ['a'] },
c: { type: Number, required: true, union: ['d'] },
d: { type: String, required: false, union: ['c'] }
},
setup(__props: any, { expose: __expose }) {
__expose();
const props = __props;
return { props }
}
})"
`;

exports[`defineProps > distributive union with boolean keys 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
export default /*@__PURE__*/_defineComponent({
props: {
a: { type: String, required: true, union: ['b'] },
b: { type: Number, required: true, union: ['a'] },
c: { type: Boolean, required: true, union: ['d'] },
d: { type: String, required: true, union: ['c'] }
},
setup(__props: any, { expose: __expose }) {
__expose();
const props = __props;
return { props }
}
})"
`;

exports[`defineProps > should escape names w/ special symbols 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
Expand Down
48 changes: 48 additions & 0 deletions packages/compiler-sfc/__tests__/compileScript/defineProps.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,54 @@ const props = defineProps({ foo: String })
assertCode(content)
})

test('distributive union w/ conditional keys', () => {
const { content } = compile(`
<script setup lang="ts">
const props = withDefaults(defineProps<{
a?: string;
b: number;
} | {
c: number;
d?: string;
}>(), {
});
</script>
`)
assertCode(content)
})

test('discriminated union', () => {
const { content } = compile(`
<script setup lang="ts">
const props = withDefaults(defineProps<{
tag: string;
d1: number;
} | {
tag: number;
d2: string;
}>(), {
});
</script>
`)
assertCode(content)
})

test('distributive union with boolean keys', () => {
const { content } = compile(`
<script setup lang="ts">
const props = withDefaults(defineProps<{
a: string;
b: number;
} | {
c: boolean;
d: string;
}>(), {
});
</script>
`)
assertCode(content)
})

// #7111
test('withDefaults (static) w/ production mode', () => {
const { content } = compile(
Expand Down
20 changes: 19 additions & 1 deletion packages/compiler-sfc/src/script/defineProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
import { genModelProps } from './defineModel'
import { getObjectOrArrayExpressionKeys } from './analyzeScriptBindings'
import { processPropsDestructure } from './definePropsDestructure'
import { isArray } from '@vue/shared'

export const DEFINE_PROPS = 'defineProps'
export const WITH_DEFAULTS = 'withDefaults'
Expand All @@ -34,8 +35,11 @@ export interface PropTypeData {
type: string[]
required: boolean
skipCheck: boolean
union?: UnionDefinition
}

export type UnionDefinition = string[] | string[][]

export type PropsDestructureBindings = Record<
string, // public prop key
{
Expand Down Expand Up @@ -231,6 +235,7 @@ function resolveRuntimePropsFromType(
key,
required: !e.optional,
type: type || [`null`],
union: e.union,
skipCheck,
})
}
Expand All @@ -239,7 +244,7 @@ function resolveRuntimePropsFromType(

function genRuntimePropFromType(
ctx: TypeResolveContext,
{ key, required, type, skipCheck }: PropTypeData,
{ key, required, type, skipCheck, union }: PropTypeData,
hasStaticDefaults: boolean,
): string {
let defaultString: string | undefined
Expand Down Expand Up @@ -272,6 +277,7 @@ function genRuntimePropFromType(
return `${finalKey}: { ${concatStrings([
`type: ${toRuntimeTypeString(type)}`,
`required: ${required}`,
union && `union: ${genUnionArrayString(union)}`,
skipCheck && 'skipCheck: true',
defaultString,
])} }`
Expand Down Expand Up @@ -306,6 +312,18 @@ function genRuntimePropFromType(
}
}

function genUnionArrayString(union: UnionDefinition): string {
const entries = union.map(key => {
if (isArray(key)) {
return genUnionArrayString(key)
} else {
return `'${key}'`
}
})

return `[${entries.join(', ')}]`
}

/**
* check defaults. If the default object is an object literal with only
* static properties, we can directly generate more optimized default
Expand Down
30 changes: 21 additions & 9 deletions packages/compiler-sfc/src/script/resolveType.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import type TS from 'typescript'
import { dirname, extname, join } from 'path'
import { minimatch as isMatch } from 'minimatch'
import * as process from 'process'
import type { UnionDefinition } from './defineProps'

export type SimpleTypeResolveOptions = Partial<
Pick<
Expand Down Expand Up @@ -125,14 +126,16 @@ export interface MaybeWithScope {
_ownerScope?: TypeScope
}

interface ResolvedElements {
props: Record<
string,
(TSPropertySignature | TSMethodSignature) & {
// resolved props always has ownerScope attached
_ownerScope: TypeScope
}
>
export interface MaybeWithUnion {
union?: UnionDefinition
}

export type ResolvedElementProp = (TSPropertySignature | TSMethodSignature) &
WithScope &
MaybeWithUnion

export interface ResolvedElements {
props: Record<string, ResolvedElementProp>
calls?: (TSCallSignatureDeclaration | TSFunctionType)[]
}

Expand Down Expand Up @@ -368,6 +371,9 @@ function mergeElements(
for (const key in props) {
if (!hasOwn(baseProps, key)) {
baseProps[key] = props[key]

const keys = Object.keys(props).filter(k => k !== key)
baseProps[key].union = keys
} else {
baseProps[key] = createProperty(
baseProps[key].key,
Expand All @@ -378,6 +384,10 @@ function mergeElements(
},
baseProps[key]._ownerScope,
baseProps[key].optional || props[key].optional,
[
baseProps[key].union || [],
Object.keys(props).filter(k => k !== key),
],
)
}
}
Expand All @@ -393,7 +403,8 @@ function createProperty(
typeAnnotation: TSType,
scope: TypeScope,
optional: boolean,
): TSPropertySignature & WithScope {
union?: UnionDefinition,
): TSPropertySignature & WithScope & MaybeWithUnion {
return {
type: 'TSPropertySignature',
key,
Expand All @@ -404,6 +415,7 @@ function createProperty(
typeAnnotation,
},
_ownerScope: scope,
union,
}
}

Expand Down
Loading

0 comments on commit eae96dc

Please sign in to comment.