feat: types for <script setup> macros

This commit is contained in:
Evan You 2022-06-16 10:19:01 +08:00
parent 3c2707b62e
commit 7173ad4272
8 changed files with 337 additions and 12 deletions

View File

@ -467,7 +467,7 @@ export function resolveAsset(
// fallback to prototype chain
const res = assets[id] || assets[camelizedId] || assets[PascalCaseId]
if (__DEV__ && warnMissing && !res) {
warn('Failed to resolve ' + type.slice(0, -1) + ': ' + id, options)
warn('Failed to resolve ' + type.slice(0, -1) + ': ' + id)
}
return res
}

View File

@ -26,7 +26,9 @@ export type ComponentOptions = {
// data
data: object | Function | void
props?: { [key: string]: PropOptions }
props?:
| string[]
| Record<string, Function | Array<Function> | null | PropOptions>
propsData?: object
computed?: {
[key: string]:
@ -105,8 +107,8 @@ export type ComponentOptions = {
}
export type PropOptions = {
type: Function | Array<Function> | null
default: any
required: boolean | null
validator: Function | null
type?: Function | Array<Function> | null
default?: any
required?: boolean | null
validator?: Function | null
}

View File

@ -1,7 +1,14 @@
import { Component } from 'types/component'
import { PropOptions } from 'types/options'
import { def, invokeWithErrorHandling, isReserved, warn } from '../core/util'
import VNode from '../core/vdom/vnode'
import { bind, emptyObject, isFunction, isObject } from '../shared/util'
import {
bind,
emptyObject,
isArray,
isFunction,
isObject
} from '../shared/util'
import { currentInstance, setCurrentInstance } from './currentInstance'
import { isRef } from './reactivity/ref'
@ -193,3 +200,35 @@ function getContext(): SetupContext {
const vm = currentInstance!
return vm._setupContext || (vm._setupContext = createSetupContext(vm))
}
/**
* Runtime helper for merging default declarations. Imported by compiled code
* only.
* @internal
*/
export function mergeDefaults(
raw: string[] | Record<string, PropOptions>,
defaults: Record<string, any>
): Record<string, PropOptions> {
const props = isArray(raw)
? raw.reduce(
(normalized, p) => ((normalized[p] = {}), normalized),
{} as Record<string, PropOptions>
)
: raw
for (const key in defaults) {
const opt = props[key]
if (opt) {
if (isArray(opt) || isFunction(opt)) {
props[key] = { type: opt, default: defaults[key] }
} else {
opt.default = defaults[key]
}
} else if (opt === null) {
props[key] = { default: defaults[key] }
} else if (__DEV__) {
warn(`props default key "${key}" has no corresponding declaration.`)
}
}
return props
}

View File

@ -179,10 +179,8 @@ function doWatch(
const warnInvalidSource = (s: unknown) => {
warn(
`Invalid watch source: `,
s,
`A watch source can only be a getter/effect function, a ref, ` +
`a reactive object, or an array of these types.`
`Invalid watch source: ${s}. A watch source can only be a getter/effect ` +
`function, a ref, a reactive object, or an array of these types.`
)
}

View File

@ -70,7 +70,7 @@ export { provide, inject, InjectionKey } from './apiInject'
export { h } from './h'
export { getCurrentInstance } from './currentInstance'
export { useSlots, useAttrs } from './apiSetup'
export { useSlots, useAttrs, mergeDefaults } from './apiSetup'
export { nextTick } from 'core/util/next-tick'
export { set, del } from 'core/observer'

2
types/index.d.ts vendored
View File

@ -35,6 +35,8 @@ export {
export * from './v3-manual-apis'
export * from './v3-generated'
// <script setup> helpers
export * from './v3-setup-helpers'
export { Data } from './common'
export { SetupContext } from './v3-setup-context'

View File

@ -0,0 +1,134 @@
import { useAttrs, useSlots, SetupContext } from '../index'
import { describe, expectType } from './utils'
describe('defineProps w/ type declaration', () => {
// type declaration
const props = defineProps<{
foo: string
}>()
// explicitly declared type should be refined
expectType<string>(props.foo)
// @ts-expect-error
props.bar
})
describe('defineProps w/ type declaration + withDefaults', () => {
const res = withDefaults(
defineProps<{
number?: number
arr?: string[]
obj?: { x: number }
fn?: (e: string) => void
x?: string
genStr?: string
}>(),
{
number: 123,
arr: () => [],
obj: () => ({ x: 123 }),
fn: () => {},
genStr: () => ''
}
)
res.number + 1
res.arr.push('hi')
res.obj.x
res.fn('hi')
// @ts-expect-error
res.x.slice()
res.genStr.slice()
})
describe('defineProps w/ union type declaration + withDefaults', () => {
withDefaults(
defineProps<{
union1?: number | number[] | { x: number }
union2?: number | number[] | { x: number }
union3?: number | number[] | { x: number }
union4?: number | number[] | { x: number }
}>(),
{
union1: 123,
union2: () => [123],
union3: () => ({ x: 123 }),
union4: () => 123
}
)
})
describe('defineProps w/ runtime declaration', () => {
// runtime declaration
const props = defineProps({
foo: String,
bar: {
type: Number,
default: 1
},
baz: {
type: Array,
required: true
}
})
expectType<{
foo?: string
bar: number
baz: unknown[]
}>(props)
props.foo && props.foo + 'bar'
props.bar + 1
// @ts-expect-error should be readonly
props.bar++
props.baz.push(1)
const props2 = defineProps(['foo', 'bar'])
props2.foo + props2.bar
// @ts-expect-error
props2.baz
})
describe('defineEmits w/ type declaration', () => {
const emit = defineEmits<(e: 'change') => void>()
emit('change')
// @ts-expect-error
emit()
// @ts-expect-error
emit('bar')
type Emits = { (e: 'foo' | 'bar'): void; (e: 'baz', id: number): void }
const emit2 = defineEmits<Emits>()
emit2('foo')
emit2('bar')
emit2('baz', 123)
// @ts-expect-error
emit2('baz')
})
describe('defineEmits w/ runtime declaration', () => {
const emit = defineEmits({
foo: () => {},
bar: null
})
emit('foo')
emit('bar', 123)
// @ts-expect-error
emit('baz')
const emit2 = defineEmits(['foo', 'bar'])
emit2('foo')
emit2('bar', 123)
// @ts-expect-error
emit2('baz')
})
describe('useAttrs', () => {
const attrs = useAttrs()
expectType<Record<string, unknown>>(attrs)
})
describe('useSlots', () => {
const slots = useSlots()
expectType<SetupContext['slots']>(slots)
})

150
types/v3-setup-helpers.d.ts vendored Normal file
View File

@ -0,0 +1,150 @@
import { EmitFn, EmitsOptions } from './v3-setup-context'
import {
ComponentObjectPropsOptions,
ExtractPropTypes
} from './v3-component-props'
/**
* 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 declaration:
* ```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>
/**
* Vue `<script setup>` compiler macro for declaring a component's emitted
* events. The expected argument is the same as the component `emits` option.
*
* Example runtime declaration:
* ```js
* const emit = defineEmits(['change', 'update'])
* ```
*
* Example type-based declaration:
* ```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
/**
* 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. variables 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 extends Record<string, any> = Record<string, any>
>(exposed?: Exposed): void
type NotUndefined<T> = T extends undefined ? never : T
type InferDefaults<T> = {
[K in keyof T]?: InferDefault<T, NotUndefined<T[K]>>
}
type InferDefault<P, T> = T extends
| null
| number
| string
| boolean
| symbol
| Function
? T | ((props: P) => T)
: (props: P) => T
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` declaration.
*
* 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>
// make them global
type _defineProps = typeof defineProps
type _defineEmits = typeof defineEmits
type _defineExpose = typeof defineExpose
type _withDefaults = typeof withDefaults
declare global {
const defineProps: _defineProps
const defineEmits: _defineEmits
const defineExpose: _defineExpose
const withDefaults: _withDefaults
}