feat(sfc): withDefaults helper

This commit is contained in:
Evan You 2021-06-26 21:11:57 -04:00
parent 3ffc7be864
commit 4c5844a9ca
9 changed files with 492 additions and 165 deletions

View File

@ -881,3 +881,50 @@ return { }
})" })"
`; `;
exports[`SFC compile <script setup> with TypeScript withDefaults (dynamic) 1`] = `
"import { mergeDefaults as _mergeDefaults, defineComponent as _defineComponent } from 'vue'
import { defaults } from './foo'
export default _defineComponent({
props: _mergeDefaults({
foo: { type: String, required: false },
bar: { type: Number, required: false }
}, { ...defaults }) as unknown as undefined,
setup(__props: {
foo?: string
bar?: number
}, { expose }) {
expose()
const props = __props
return { props, defaults }
}
})"
`;
exports[`SFC compile <script setup> with TypeScript withDefaults (static) 1`] = `
"import { defineComponent as _defineComponent } from 'vue'
export default _defineComponent({
props: {
foo: { type: String, required: false, default: 'hi' },
bar: { type: Number, required: false }
} as unknown as undefined,
setup(__props: {
foo?: string
bar?: number
}, { expose }) {
expose()
const props = __props
return { props }
}
})"
`;

View File

@ -592,6 +592,51 @@ const emit = defineEmits(['a', 'b'])
}) })
}) })
test('withDefaults (static)', () => {
const { content, bindings } = compile(`
<script setup lang="ts">
const props = withDefaults(defineProps<{
foo?: string
bar?: number
}>(), {
foo: 'hi'
})
</script>
`)
assertCode(content)
expect(content).toMatch(
`foo: { type: String, required: false, default: 'hi' }`
)
expect(content).toMatch(`bar: { type: Number, required: false }`)
expect(content).toMatch(`const props = __props`)
expect(bindings).toStrictEqual({
foo: BindingTypes.PROPS,
bar: BindingTypes.PROPS,
props: BindingTypes.SETUP_CONST
})
})
test('withDefaults (dynamic)', () => {
const { content } = compile(`
<script setup lang="ts">
import { defaults } from './foo'
const props = withDefaults(defineProps<{
foo?: string
bar?: number
}>(), { ...defaults })
</script>
`)
assertCode(content)
expect(content).toMatch(`import { mergeDefaults as _mergeDefaults`)
expect(content).toMatch(
`
_mergeDefaults({
foo: { type: String, required: false },
bar: { type: Number, required: false }
}, { ...defaults })`.trim()
)
})
test('defineEmits w/ type', () => { test('defineEmits w/ type', () => {
const { content } = compile(` const { content } = compile(`
<script setup lang="ts"> <script setup lang="ts">
@ -942,7 +987,6 @@ const emit = defineEmits(['a', 'b'])
test('defineProps/Emit() w/ both type and non-type args', () => { test('defineProps/Emit() w/ both type and non-type args', () => {
expect(() => { expect(() => {
compile(`<script setup lang="ts"> compile(`<script setup lang="ts">
import { defineProps } from 'vue'
defineProps<{}>({}) defineProps<{}>({})
</script>`) </script>`)
}).toThrow(`cannot accept both type and non-type arguments`) }).toThrow(`cannot accept both type and non-type arguments`)

View File

@ -36,8 +36,11 @@ import { rewriteDefault } from './rewriteDefault'
const DEFINE_PROPS = 'defineProps' const DEFINE_PROPS = 'defineProps'
const DEFINE_EMIT = 'defineEmit' const DEFINE_EMIT = 'defineEmit'
const DEFINE_EMITS = 'defineEmits'
const DEFINE_EXPOSE = 'defineExpose' const DEFINE_EXPOSE = 'defineExpose'
const WITH_DEFAULTS = 'withDefaults'
// deprecated
const DEFINE_EMITS = 'defineEmits'
export interface SFCScriptCompileOptions { export interface SFCScriptCompileOptions {
/** /**
@ -191,6 +194,7 @@ export function compileScript(
let hasDefineEmitCall = false let hasDefineEmitCall = false
let hasDefineExposeCall = false let hasDefineExposeCall = false
let propsRuntimeDecl: Node | undefined let propsRuntimeDecl: Node | undefined
let propsRuntimeDefaults: Node | undefined
let propsTypeDecl: TSTypeLiteral | undefined let propsTypeDecl: TSTypeLiteral | undefined
let propsIdentifier: string | undefined let propsIdentifier: string | undefined
let emitRuntimeDecl: Node | undefined let emitRuntimeDecl: Node | undefined
@ -262,13 +266,18 @@ export function compileScript(
} }
function processDefineProps(node: Node): boolean { function processDefineProps(node: Node): boolean {
if (isCallOf(node, DEFINE_PROPS)) { if (!isCallOf(node, DEFINE_PROPS)) {
return false
}
if (hasDefinePropsCall) { if (hasDefinePropsCall) {
error(`duplicate ${DEFINE_PROPS}() call`, node) error(`duplicate ${DEFINE_PROPS}() call`, node)
} }
hasDefinePropsCall = true hasDefinePropsCall = true
propsRuntimeDecl = node.arguments[0] propsRuntimeDecl = node.arguments[0]
// context call has type parameters - infer runtime types from it
// call has type parameters - infer runtime types from it
if (node.typeParameters) { if (node.typeParameters) {
if (propsRuntimeDecl) { if (propsRuntimeDecl) {
error( error(
@ -277,6 +286,7 @@ export function compileScript(
node node
) )
} }
const typeArg = node.typeParameters.params[0] const typeArg = node.typeParameters.params[0]
if (typeArg.type === 'TSTypeLiteral') { if (typeArg.type === 'TSTypeLiteral') {
propsTypeDecl = typeArg propsTypeDecl = typeArg
@ -287,13 +297,36 @@ export function compileScript(
) )
} }
} }
return true return true
} }
function processWithDefaults(node: Node): boolean {
if (!isCallOf(node, WITH_DEFAULTS)) {
return false return false
} }
if (processDefineProps(node.arguments[0])) {
if (propsRuntimeDecl) {
error(
`${WITH_DEFAULTS} can only be used with type-based ` +
`${DEFINE_PROPS} declaration.`,
node
)
}
propsRuntimeDefaults = node.arguments[1]
} else {
error(
`${WITH_DEFAULTS}' first argument must be a ${DEFINE_PROPS} call.`,
node.arguments[0] || node
)
}
return true
}
function processDefineEmits(node: Node): boolean { function processDefineEmits(node: Node): boolean {
if (isCallOf(node, DEFINE_EMIT) || isCallOf(node, DEFINE_EMITS)) { if (!isCallOf(node, c => c === DEFINE_EMIT || c === DEFINE_EMITS)) {
return false
}
if (hasDefineEmitCall) { if (hasDefineEmitCall) {
error(`duplicate ${DEFINE_EMITS}() call`, node) error(`duplicate ${DEFINE_EMITS}() call`, node)
} }
@ -323,8 +356,6 @@ export function compileScript(
} }
return true return true
} }
return false
}
function processDefineExpose(node: Node): boolean { function processDefineExpose(node: Node): boolean {
if (isCallOf(node, DEFINE_EXPOSE)) { if (isCallOf(node, DEFINE_EXPOSE)) {
@ -480,6 +511,63 @@ export function compileScript(
} }
} }
function genRuntimeProps(props: Record<string, PropTypeData>) {
const keys = Object.keys(props)
if (!keys.length) {
return ``
}
// check defaults. If the default object is an object literal with only
// static properties, we can directly generate more optimzied default
// decalrations. Otherwise we will have to fallback to runtime merging.
const hasStaticDefaults =
propsRuntimeDefaults &&
propsRuntimeDefaults.type === 'ObjectExpression' &&
propsRuntimeDefaults.properties.every(
node => node.type === 'ObjectProperty' && !node.computed
)
let propsDecls = `{
${keys
.map(key => {
let defaultString: string | undefined
if (hasStaticDefaults) {
const prop = (propsRuntimeDefaults as ObjectExpression).properties.find(
(node: any) => node.key.name === key
) as ObjectProperty
if (prop) {
// prop has corresponding static default value
defaultString = `default: ${source.slice(
prop.value.start! + startOffset,
prop.value.end! + startOffset
)}`
}
}
if (__DEV__) {
const { type, required } = props[key]
return `${key}: { type: ${toRuntimeTypeString(
type
)}, required: ${required}${
defaultString ? `, ${defaultString}` : ``
} }`
} else {
// production: checks are useless
return `${key}: ${defaultString ? `{ ${defaultString} }` : 'null'}`
}
})
.join(',\n ')}\n }`
if (propsRuntimeDefaults && !hasStaticDefaults) {
propsDecls = `${helper('mergeDefaults')}(${propsDecls}, ${source.slice(
propsRuntimeDefaults.start! + startOffset,
propsRuntimeDefaults.end! + startOffset
)})`
}
return `\n props: ${propsDecls} as unknown as undefined,`
}
// 1. process normal <script> first if it exists // 1. process normal <script> first if it exists
let scriptAst let scriptAst
if (script) { if (script) {
@ -675,7 +763,8 @@ export function compileScript(
// process `defineProps` and `defineEmit(s)` calls // process `defineProps` and `defineEmit(s)` calls
if ( if (
processDefineProps(node.expression) || processDefineProps(node.expression) ||
processDefineEmits(node.expression) processDefineEmits(node.expression) ||
processWithDefaults(node.expression)
) { ) {
s.remove(node.start! + startOffset, node.end! + startOffset) s.remove(node.start! + startOffset, node.end! + startOffset)
} else if (processDefineExpose(node.expression)) { } else if (processDefineExpose(node.expression)) {
@ -692,7 +781,8 @@ export function compileScript(
if (node.type === 'VariableDeclaration' && !node.declare) { if (node.type === 'VariableDeclaration' && !node.declare) {
for (const decl of node.declarations) { for (const decl of node.declarations) {
if (decl.init) { if (decl.init) {
const isDefineProps = processDefineProps(decl.init) const isDefineProps =
processDefineProps(decl.init) || processWithDefaults(decl.init)
if (isDefineProps) { if (isDefineProps) {
propsIdentifier = scriptSetup.content.slice( propsIdentifier = scriptSetup.content.slice(
decl.id.start!, decl.id.start!,
@ -812,6 +902,7 @@ export function compileScript(
// 5. check useOptions args to make sure it doesn't reference setup scope // 5. check useOptions args to make sure it doesn't reference setup scope
// variables // variables
checkInvalidScopeReference(propsRuntimeDecl, DEFINE_PROPS) checkInvalidScopeReference(propsRuntimeDecl, DEFINE_PROPS)
checkInvalidScopeReference(propsRuntimeDefaults, DEFINE_PROPS)
checkInvalidScopeReference(emitRuntimeDecl, DEFINE_PROPS) checkInvalidScopeReference(emitRuntimeDecl, DEFINE_PROPS)
// 6. remove non-script content // 6. remove non-script content
@ -1080,9 +1171,14 @@ function walkDeclaration(
for (const { id, init } of node.declarations) { for (const { id, init } of node.declarations) {
const isDefineCall = !!( const isDefineCall = !!(
isConst && isConst &&
(isCallOf(init, DEFINE_PROPS) || isCallOf(
isCallOf(init, DEFINE_EMIT) || init,
isCallOf(init, DEFINE_EMITS)) c =>
c === DEFINE_PROPS ||
c === DEFINE_EMIT ||
c === DEFINE_EMITS ||
c === WITH_DEFAULTS
)
) )
if (id.type === 'Identifier') { if (id.type === 'Identifier') {
let bindingType let bindingType
@ -1318,29 +1414,6 @@ function inferRuntimeType(
} }
} }
function genRuntimeProps(props: Record<string, PropTypeData>) {
const keys = Object.keys(props)
if (!keys.length) {
return ``
}
if (!__DEV__) {
// production: generate array version only
return `\n props: [\n ${keys
.map(k => JSON.stringify(k))
.join(',\n ')}\n ] as unknown as undefined,`
}
return `\n props: {\n ${keys
.map(key => {
const { type, required } = props[key]
return `${key}: { type: ${toRuntimeTypeString(
type
)}, required: ${required} }`
})
.join(',\n ')}\n } as unknown as undefined,`
}
function toRuntimeTypeString(types: string[]) { function toRuntimeTypeString(types: string[]) {
return types.some(t => t === 'null') return types.some(t => t === 'null')
? `null` ? `null`
@ -1567,13 +1640,15 @@ function isFunction(node: Node): node is FunctionNode {
function isCallOf( function isCallOf(
node: Node | null | undefined, node: Node | null | undefined,
name: string test: string | ((id: string) => boolean)
): node is CallExpression { ): node is CallExpression {
return !!( return !!(
node && node &&
node.type === 'CallExpression' && node.type === 'CallExpression' &&
node.callee.type === 'Identifier' && node.callee.type === 'Identifier' &&
node.callee.name === name (typeof test === 'string'
? node.callee.name === test
: test(node.callee.name))
) )
} }

View File

@ -8,8 +8,11 @@ import {
import { import {
defineEmits, defineEmits,
defineProps, defineProps,
defineExpose,
withDefaults,
useAttrs, useAttrs,
useSlots useSlots,
mergeDefaults
} from '../src/apiSetupHelpers' } from '../src/apiSetupHelpers'
describe('SFC <script setup> helpers', () => { describe('SFC <script setup> helpers', () => {
@ -19,6 +22,12 @@ describe('SFC <script setup> helpers', () => {
defineEmits() defineEmits()
expect(`defineEmits() is a compiler-hint`).toHaveBeenWarned() expect(`defineEmits() is a compiler-hint`).toHaveBeenWarned()
defineExpose()
expect(`defineExpose() is a compiler-hint`).toHaveBeenWarned()
withDefaults({}, {})
expect(`withDefaults() is a compiler-hint`).toHaveBeenWarned()
}) })
test('useSlots / useAttrs (no args)', () => { test('useSlots / useAttrs (no args)', () => {
@ -58,4 +67,26 @@ describe('SFC <script setup> helpers', () => {
expect(slots).toBe(ctx!.slots) expect(slots).toBe(ctx!.slots)
expect(attrs).toBe(ctx!.attrs) expect(attrs).toBe(ctx!.attrs)
}) })
test('mergeDefaults', () => {
const merged = mergeDefaults(
{
foo: null,
bar: { type: String, required: false }
},
{
foo: 1,
bar: 'baz'
}
)
expect(merged).toMatchObject({
foo: { default: 1 },
bar: { type: String, required: false, default: 'baz' }
})
mergeDefaults({}, { foo: 1 })
expect(
`props default key "foo" has no corresponding declaration`
).toHaveBeenWarned()
})
}) })

View File

@ -4,63 +4,104 @@ import {
createSetupContext createSetupContext
} from './component' } from './component'
import { EmitFn, EmitsOptions } from './componentEmits' import { EmitFn, EmitsOptions } from './componentEmits'
import { ComponentObjectPropsOptions, ExtractPropTypes } from './componentProps' import {
ComponentObjectPropsOptions,
PropOptions,
ExtractPropTypes
} from './componentProps'
import { warn } from './warning' import { warn } from './warning'
type InferDefaults<T> = { // dev only
[K in keyof T]?: NonNullable<T[K]> extends object const warnRuntimeUsage = (method: string) =>
? () => NonNullable<T[K]>
: NonNullable<T[K]>
}
/**
* Compile-time-only helper used for declaring props inside `<script setup>`.
* This is stripped away in the compiled code and should never be actually
* called at runtime.
*/
// overload 1: string props
export function defineProps<
TypeProps = undefined,
PropNames extends string = string,
InferredProps = { [key in PropNames]?: any }
>(
props?: PropNames[]
): Readonly<TypeProps extends undefined ? InferredProps : TypeProps>
// overload 2: object props
export function defineProps<
TypeProps = undefined,
PP extends ComponentObjectPropsOptions = ComponentObjectPropsOptions,
InferredProps = ExtractPropTypes<PP>
>(
props?: PP,
defaults?: InferDefaults<TypeProps>
): Readonly<TypeProps extends undefined ? InferredProps : TypeProps>
// implementation
export function defineProps() {
if (__DEV__) {
warn( warn(
`defineProps() is a compiler-hint helper that is only usable inside ` + `${method}() is a compiler-hint helper that is only usable inside ` +
`<script setup> of a single file component. Its arguments should be ` + `<script setup> of a single file component. Its arguments should be ` +
`compiled away and passing it at runtime has no effect.` `compiled away and passing it at runtime has no effect.`
) )
/**
* Vue `<script setup>` compiler macro for declaring component props. The
* expected argument is the same as the component `props` option.
*
* Example runtime declaration:
* ```js
* // using Array syntax
* const props = defineProps(['foo', 'bar'])
* // using Object syntax
* const props = defineProps({
* foo: String,
* bar: {
* type: Number,
* required: true
* }
* })
* ```
*
* Equivalent type-based decalration:
* ```ts
* // will be compiled into equivalent runtime declarations
* const props = defineProps<{
* foo?: string
* bar: number
* }>()
* ```
*
* This is only usable inside `<script setup>`, is compiled away in the
* output and should **not** be actually called at runtime.
*/
// overload 1: runtime props w/ array
export function defineProps<PropNames extends string = string>(
props: PropNames[]
): Readonly<{ [key in PropNames]?: any }>
// overload 2: runtime props w/ object
export function defineProps<
PP extends ComponentObjectPropsOptions = ComponentObjectPropsOptions
>(props: PP): Readonly<ExtractPropTypes<PP>>
// overload 3: typed-based declaration
export function defineProps<TypeProps>(): Readonly<TypeProps>
// implementation
export function defineProps() {
if (__DEV__) {
warnRuntimeUsage(`defineProps`)
} }
return null as any return null as any
} }
export function defineEmits< /**
TypeEmit = undefined, * Vue `<script setup>` compiler macro for declaring a component's emitted
E extends EmitsOptions = EmitsOptions, * events. The expected argument is the same as the component `emits` option.
EE extends string = string, *
InferredEmit = EmitFn<E> * Example runtime declaration:
>(emitOptions?: E | EE[]): TypeEmit extends undefined ? InferredEmit : TypeEmit * ```js
* const emit = defineEmits(['change', 'update'])
* ```
*
* Example type-based decalration:
* ```ts
* const emit = defineEmits<{
* (event: 'change'): void
* (event: 'update', id: number): void
* }>()
*
* emit('change')
* emit('update', 1)
* ```
*
* This is only usable inside `<script setup>`, is compiled away in the
* output and should **not** be actually called at runtime.
*/
// overload 1: runtime emits w/ array
export function defineEmits<EE extends string = string>(
emitOptions: EE[]
): EmitFn<EE[]>
export function defineEmits<E extends EmitsOptions = EmitsOptions>(
emitOptions: E
): EmitFn<E>
export function defineEmits<TypeEmit>(): TypeEmit
// implementation // implementation
export function defineEmits() { export function defineEmits() {
if (__DEV__) { if (__DEV__) {
warn( warnRuntimeUsage(`defineEmits`)
`defineEmits() is a compiler-hint helper that is only usable inside ` +
`<script setup> of a single file component. Its arguments should be ` +
`compiled away and passing it at runtime has no effect.`
)
} }
return null as any return null as any
} }
@ -70,16 +111,70 @@ export function defineEmits() {
*/ */
export const defineEmit = defineEmits export const defineEmit = defineEmits
/**
* Vue `<script setup>` compiler macro for declaring a component's exposed
* instance properties when it is accessed by a parent component via template
* refs.
*
* `<script setup>` components are closed by default - i.e. varaibles inside
* the `<script setup>` scope is not exposed to parent unless explicitly exposed
* via `defineExpose`.
*
* This is only usable inside `<script setup>`, is compiled away in the
* output and should **not** be actually called at runtime.
*/
export function defineExpose(exposed?: Record<string, any>) { export function defineExpose(exposed?: Record<string, any>) {
if (__DEV__) { if (__DEV__) {
warn( warnRuntimeUsage(`defineExpose`)
`defineExpose() is a compiler-hint helper that is only usable inside ` +
`<script setup> of a single file component. Its usage should be ` +
`compiled away and calling it at runtime has no effect.`
)
} }
} }
type NotUndefined<T> = T extends undefined ? never : T
type InferDefaults<T> = {
[K in keyof T]?: NotUndefined<T[K]> extends (
| number
| string
| boolean
| symbol
| Function)
? NotUndefined<T[K]>
: (props: T) => NotUndefined<T[K]>
}
type PropsWithDefaults<Base, Defaults> = Base &
{
[K in keyof Defaults]: K extends keyof Base ? NotUndefined<Base[K]> : never
}
/**
* Vue `<script setup>` compiler macro for providing props default values when
* using type-based `defineProps` decalration.
*
* Example usage:
* ```ts
* withDefaults(defineProps<{
* size?: number
* labels?: string[]
* }>(), {
* size: 3,
* labels: () => ['default label']
* })
* ```
*
* This is only usable inside `<script setup>`, is compiled away in the output
* and should **not** be actually called at runtime.
*/
export function withDefaults<Props, Defaults extends InferDefaults<Props>>(
props: Props,
defaults: Defaults
): PropsWithDefaults<Props, Defaults> {
if (__DEV__) {
warnRuntimeUsage(`withDefaults`)
}
return null as any
}
/** /**
* @deprecated use `useSlots` and `useAttrs` instead. * @deprecated use `useSlots` and `useAttrs` instead.
*/ */
@ -93,6 +188,14 @@ export function useContext(): SetupContext {
return getContext() return getContext()
} }
export function useSlots(): SetupContext['slots'] {
return getContext().slots
}
export function useAttrs(): SetupContext['attrs'] {
return getContext().attrs
}
function getContext(): SetupContext { function getContext(): SetupContext {
const i = getCurrentInstance()! const i = getCurrentInstance()!
if (__DEV__ && !i) { if (__DEV__ && !i) {
@ -101,10 +204,25 @@ function getContext(): SetupContext {
return i.setupContext || (i.setupContext = createSetupContext(i)) return i.setupContext || (i.setupContext = createSetupContext(i))
} }
export function useSlots(): SetupContext['slots'] { /**
return getContext().slots * Runtime helper for merging default declarations. Imported by compiled code
* only.
* @internal
*/
export function mergeDefaults(
// the base props is compiler-generated and guaranteed to be in this shape.
props: Record<string, PropOptions | null>,
defaults: Record<string, any>
) {
for (const key in defaults) {
const val = props[key]
if (val) {
val.default = defaults[key]
} else if (val === null) {
props[key] = { default: defaults[key] }
} else if (__DEV__) {
warn(`props default key "${key}" has no corresponding declaration.`)
} }
}
export function useAttrs(): SetupContext['attrs'] { return props
return getContext().attrs
} }

View File

@ -51,7 +51,7 @@ export type Prop<T, D = T> = PropOptions<T, D> | PropType<T>
type DefaultFactory<T> = (props: Data) => T | null | undefined type DefaultFactory<T> = (props: Data) => T | null | undefined
interface PropOptions<T = any, D = T> { export interface PropOptions<T = any, D = T> {
type?: PropType<T> | true | null type?: PropType<T> | true | null
required?: boolean required?: boolean
default?: D | DefaultFactory<D> | null | undefined | object default?: D | DefaultFactory<D> | null | undefined | object

View File

@ -44,9 +44,17 @@ export { provide, inject } from './apiInject'
export { nextTick } from './scheduler' export { nextTick } from './scheduler'
export { defineComponent } from './apiDefineComponent' export { defineComponent } from './apiDefineComponent'
export { defineAsyncComponent } from './apiAsyncComponent' export { defineAsyncComponent } from './apiAsyncComponent'
// <script setup> API ----------------------------------------------------------
export { export {
defineProps, defineProps,
defineEmits, defineEmits,
defineExpose,
withDefaults,
// internal
mergeDefaults,
// deprecated
defineEmit, defineEmit,
useContext useContext
} from './apiSetupHelpers' } from './apiSetupHelpers'
@ -140,7 +148,6 @@ export {
DeepReadonly DeepReadonly
} from '@vue/reactivity' } from '@vue/reactivity'
export { export {
// types
WatchEffect, WatchEffect,
WatchOptions, WatchOptions,
WatchOptionsBase, WatchOptionsBase,

View File

@ -214,7 +214,12 @@ async function doCompileScript(
return [code, compiledScript.bindings] return [code, compiledScript.bindings]
} catch (e) { } catch (e) {
store.errors = [e] store.errors = [
e.stack
.split('\n')
.slice(0, 12)
.join('\n')
]
return return
} }
} else { } else {

View File

@ -1,3 +1,4 @@
import { withDefaults } from '../packages/runtime-core/src/apiSetupHelpers'
import { import {
expectType, expectType,
defineProps, defineProps,
@ -19,30 +20,29 @@ describe('defineProps w/ type declaration', () => {
props.bar props.bar
}) })
describe('defineProps w/ type declaration + defaults', () => { describe('defineProps w/ type declaration + withDefaults', () => {
const res = withDefaults(
defineProps<{ defineProps<{
number?: number number?: number
arr?: string[] arr?: string[]
arr2?: string[]
obj?: { x: number } obj?: { x: number }
obj2?: { x: number } fn?: (e: string) => void
obj3?: { x: number } x?: string
}>( }>(),
{},
{ {
number: 1, number: 123,
arr: () => [],
arr: () => [''],
// @ts-expect-error not using factory
arr2: [''],
obj: () => ({ x: 123 }), obj: () => ({ x: 123 }),
// @ts-expect-error not using factory fn: () => {}
obj2: { x: 123 },
// @ts-expect-error factory return type does not match
obj3: () => ({ x: 'foo' })
} }
) )
res.number + 1
res.arr.push('hi')
res.obj.x
res.fn('hi')
// @ts-expect-error
res.x.slice()
}) })
describe('defineProps w/ runtime declaration', () => { describe('defineProps w/ runtime declaration', () => {