wip: type inference for useOptions

This commit is contained in:
Evan You 2020-11-13 00:01:44 -05:00
parent 001f8ce993
commit 6fc8d5d0ba
2 changed files with 168 additions and 6 deletions

View File

@ -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
} }

View 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')
})