+import { EmitFn, EmitsOptions } from '../componentEmits'
+import {
+ ComponentObjectPropsOptions,
+ ExtractPropTypes
+} from '../componentProps'
import { Slots } from '../componentSlots'
+import { Directive } from '../directives'
import { warn } from '../warning'
interface DefaultContext {
- props: Record<string, unknown>
+ props: {}
attrs: Record<string, unknown>
emit: (...args: any[]) => void
slots: Slots
}
+interface InferredContext<P, E> {
+ props: Readonly<P>
+ attrs: Record<string, unknown>
+ emit: EmitFn<E>
+ slots: Slots
+}
+
+type InferContext<T extends Partial<DefaultContext>, P, E> = {
+ [K in keyof DefaultContext]: T[K] extends {} ? T[K] : InferredContext<P, E>[K]
+}
+
+/**
+ * This is a subset of full options that are still useful in the context of
+ * <script setup>. Technically, other options can be used too, but are
+ * discouraged - if using TypeScript, we nudge users away from doing so by
+ * disallowing them in types.
+ */
+interface Options<E extends EmitsOptions, EE extends string> {
+ emits?: E | EE[]
+ name?: string
+ inhertiAttrs?: boolean
+ directives?: Record<string, Directive>
+}
+
/**
* Compile-time-only helper used for declaring options and retrieving props
- * and the setup context inside <script setup>.
+ * and the setup context inside `<script setup>`.
* This is stripped away in the compiled code and should never be actually
* called at runtime.
*/
-export function useOptions<T extends Partial<DefaultContext> = {}>(
- opts?: any // TODO infer
-): { [K in keyof DefaultContext]: T[K] extends {} ? T[K] : DefaultContext[K] } {
+// overload 1: no props
+export function useOptions<
+ T extends Partial<DefaultContext> = {},
+ E extends EmitsOptions = EmitsOptions,
+ EE extends string = string
+>(
+ options?: Options<E, EE> & {
+ props?: undefined
+ }
+): InferContext<T, {}, E>
+
+// overload 2: object props
+export function useOptions<
+ T extends Partial<DefaultContext> = {},
+ E extends EmitsOptions = EmitsOptions,
+ EE extends string = string,
+ PP extends string = string,
+ P = Readonly<{ [key in PP]?: any }>
+>(
+ options?: Options<E, EE> & {
+ props?: PP[]
+ }
+): InferContext<T, P, E>
+
+// overload 3: object props
+export function useOptions<
+ T extends Partial<DefaultContext> = {},
+ E extends EmitsOptions = EmitsOptions,
+ EE extends string = string,
+ PP extends ComponentObjectPropsOptions = ComponentObjectPropsOptions,
+ P = ExtractPropTypes<PP>
+>(
+ options?: Options<E, EE> & {
+ props?: PP
+ }
+): InferContext<T, P, E>
+
+// implementation
+export function useOptions() {
if (__DEV__) {
warn(
`defineContext() is a compiler-hint helper that is only usable inside ` +
`and should not be used in final distributed code.`
)
}
- return null as any
+ return 0 as any
}
--- /dev/null
+import { expectType, useOptions, Slots, describe } from './index'
+
+describe('no args', () => {
+ const { props, attrs, emit, slots } = useOptions()
+ expectType<{}>(props)
+ expectType<Record<string, unknown>>(attrs)
+ expectType<(...args: any[]) => void>(emit)
+ expectType<Slots>(slots)
+
+ // @ts-expect-error
+ props.foo
+ // should be able to emit anything
+ emit('foo')
+ emit('bar')
+})
+
+describe('with type arg', () => {
+ const { props, attrs, emit, slots } = useOptions<{
+ props: {
+ foo: string
+ }
+ emit: (e: 'change') => void
+ }>()
+
+ // explicitly declared type should be refined
+ expectType<string>(props.foo)
+ // @ts-expect-error
+ props.bar
+
+ emit('change')
+ // @ts-expect-error
+ emit()
+ // @ts-expect-error
+ emit('bar')
+
+ // non explicitly declared type should fallback to default type
+ expectType<Record<string, unknown>>(attrs)
+ expectType<Slots>(slots)
+})
+
+// with runtime arg
+describe('with runtime arg (array syntax)', () => {
+ const { props, emit } = useOptions({
+ props: ['foo', 'bar'],
+ emits: ['foo', 'bar']
+ })
+
+ expectType<{
+ foo?: any
+ bar?: any
+ }>(props)
+ // @ts-expect-error
+ props.baz
+
+ emit('foo')
+ emit('bar', 123)
+ // @ts-expect-error
+ emit('baz')
+})
+
+describe('with runtime arg (object syntax)', () => {
+ const { props, emit } = useOptions({
+ props: {
+ foo: String,
+ bar: {
+ type: Number,
+ default: 1
+ },
+ baz: {
+ type: Array,
+ required: true
+ }
+ },
+ emits: {
+ foo: () => {},
+ bar: null
+ }
+ })
+
+ 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)
+
+ emit('foo')
+ emit('bar')
+ // @ts-expect-error
+ emit('baz')
+})