wip: type inference for useOptions
This commit is contained in:
parent
001f8ce993
commit
6fc8d5d0ba
@ -1,22 +1,88 @@
|
|||||||
|
import { EmitFn, EmitsOptions } from '../componentEmits'
|
||||||
|
import {
|
||||||
|
ComponentObjectPropsOptions,
|
||||||
|
ExtractPropTypes
|
||||||
|
} from '../componentProps'
|
||||||
import { Slots } from '../componentSlots'
|
import { Slots } from '../componentSlots'
|
||||||
|
import { Directive } from '../directives'
|
||||||
import { warn } from '../warning'
|
import { warn } from '../warning'
|
||||||
|
|
||||||
interface DefaultContext {
|
interface DefaultContext {
|
||||||
props: Record<string, unknown>
|
props: {}
|
||||||
attrs: Record<string, unknown>
|
attrs: Record<string, unknown>
|
||||||
emit: (...args: any[]) => void
|
emit: (...args: any[]) => void
|
||||||
slots: Slots
|
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
|
* 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
|
* This is stripped away in the compiled code and should never be actually
|
||||||
* called at runtime.
|
* called at runtime.
|
||||||
*/
|
*/
|
||||||
export function useOptions<T extends Partial<DefaultContext> = {}>(
|
// overload 1: no props
|
||||||
opts?: any // TODO infer
|
export function useOptions<
|
||||||
): { [K in keyof DefaultContext]: T[K] extends {} ? T[K] : DefaultContext[K] } {
|
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__) {
|
if (__DEV__) {
|
||||||
warn(
|
warn(
|
||||||
`defineContext() is a compiler-hint helper that is only usable inside ` +
|
`defineContext() is a compiler-hint helper that is only usable inside ` +
|
||||||
@ -24,5 +90,5 @@ export function useOptions<T extends Partial<DefaultContext> = {}>(
|
|||||||
`and should not be used in final distributed code.`
|
`and should not be used in final distributed code.`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return null as any
|
return 0 as any
|
||||||
}
|
}
|
||||||
|
96
test-dts/useOptions.test-d.ts
Normal file
96
test-dts/useOptions.test-d.ts
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
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')
|
||||||
|
})
|
Loading…
Reference in New Issue
Block a user