Skip to content

Commit 6fc8d5d

Browse files
committed
wip: type inference for useOptions
1 parent 001f8ce commit 6fc8d5d

File tree

2 files changed

+168
-6
lines changed

2 files changed

+168
-6
lines changed
Lines changed: 72 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,94 @@
1+
import { EmitFn, EmitsOptions } from '../componentEmits'
2+
import {
3+
ComponentObjectPropsOptions,
4+
ExtractPropTypes
5+
} from '../componentProps'
16
import { Slots } from '../componentSlots'
7+
import { Directive } from '../directives'
28
import { warn } from '../warning'
39

410
interface DefaultContext {
5-
props: Record<string, unknown>
11+
props: {}
612
attrs: Record<string, unknown>
713
emit: (...args: any[]) => void
814
slots: Slots
915
}
1016

17+
interface InferredContext<P, E> {
18+
props: Readonly<P>
19+
attrs: Record<string, unknown>
20+
emit: EmitFn<E>
21+
slots: Slots
22+
}
23+
24+
type InferContext<T extends Partial<DefaultContext>, P, E> = {
25+
[K in keyof DefaultContext]: T[K] extends {} ? T[K] : InferredContext<P, E>[K]
26+
}
27+
28+
/**
29+
* This is a subset of full options that are still useful in the context of
30+
* <script setup>. Technically, other options can be used too, but are
31+
* discouraged - if using TypeScript, we nudge users away from doing so by
32+
* disallowing them in types.
33+
*/
34+
interface Options<E extends EmitsOptions, EE extends string> {
35+
emits?: E | EE[]
36+
name?: string
37+
inhertiAttrs?: boolean
38+
directives?: Record<string, Directive>
39+
}
40+
1141
/**
1242
* Compile-time-only helper used for declaring options and retrieving props
13-
* and the setup context inside <script setup>.
43+
* and the setup context inside `<script setup>`.
1444
* This is stripped away in the compiled code and should never be actually
1545
* called at runtime.
1646
*/
17-
export function useOptions<T extends Partial<DefaultContext> = {}>(
18-
opts?: any // TODO infer
19-
): { [K in keyof DefaultContext]: T[K] extends {} ? T[K] : DefaultContext[K] } {
47+
// overload 1: no props
48+
export function useOptions<
49+
T extends Partial<DefaultContext> = {},
50+
E extends EmitsOptions = EmitsOptions,
51+
EE extends string = string
52+
>(
53+
options?: Options<E, EE> & {
54+
props?: undefined
55+
}
56+
): InferContext<T, {}, E>
57+
58+
// overload 2: object props
59+
export function useOptions<
60+
T extends Partial<DefaultContext> = {},
61+
E extends EmitsOptions = EmitsOptions,
62+
EE extends string = string,
63+
PP extends string = string,
64+
P = Readonly<{ [key in PP]?: any }>
65+
>(
66+
options?: Options<E, EE> & {
67+
props?: PP[]
68+
}
69+
): InferContext<T, P, E>
70+
71+
// overload 3: object props
72+
export function useOptions<
73+
T extends Partial<DefaultContext> = {},
74+
E extends EmitsOptions = EmitsOptions,
75+
EE extends string = string,
76+
PP extends ComponentObjectPropsOptions = ComponentObjectPropsOptions,
77+
P = ExtractPropTypes<PP>
78+
>(
79+
options?: Options<E, EE> & {
80+
props?: PP
81+
}
82+
): InferContext<T, P, E>
83+
84+
// implementation
85+
export function useOptions() {
2086
if (__DEV__) {
2187
warn(
2288
`defineContext() is a compiler-hint helper that is only usable inside ` +
2389
`<script setup> of a single file component. It will be compiled away ` +
2490
`and should not be used in final distributed code.`
2591
)
2692
}
27-
return null as any
93+
return 0 as any
2894
}

test-dts/useOptions.test-d.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { expectType, useOptions, Slots, describe } from './index'
2+
3+
describe('no args', () => {
4+
const { props, attrs, emit, slots } = useOptions()
5+
expectType<{}>(props)
6+
expectType<Record<string, unknown>>(attrs)
7+
expectType<(...args: any[]) => void>(emit)
8+
expectType<Slots>(slots)
9+
10+
// @ts-expect-error
11+
props.foo
12+
// should be able to emit anything
13+
emit('foo')
14+
emit('bar')
15+
})
16+
17+
describe('with type arg', () => {
18+
const { props, attrs, emit, slots } = useOptions<{
19+
props: {
20+
foo: string
21+
}
22+
emit: (e: 'change') => void
23+
}>()
24+
25+
// explicitly declared type should be refined
26+
expectType<string>(props.foo)
27+
// @ts-expect-error
28+
props.bar
29+
30+
emit('change')
31+
// @ts-expect-error
32+
emit()
33+
// @ts-expect-error
34+
emit('bar')
35+
36+
// non explicitly declared type should fallback to default type
37+
expectType<Record<string, unknown>>(attrs)
38+
expectType<Slots>(slots)
39+
})
40+
41+
// with runtime arg
42+
describe('with runtime arg (array syntax)', () => {
43+
const { props, emit } = useOptions({
44+
props: ['foo', 'bar'],
45+
emits: ['foo', 'bar']
46+
})
47+
48+
expectType<{
49+
foo?: any
50+
bar?: any
51+
}>(props)
52+
// @ts-expect-error
53+
props.baz
54+
55+
emit('foo')
56+
emit('bar', 123)
57+
// @ts-expect-error
58+
emit('baz')
59+
})
60+
61+
describe('with runtime arg (object syntax)', () => {
62+
const { props, emit } = useOptions({
63+
props: {
64+
foo: String,
65+
bar: {
66+
type: Number,
67+
default: 1
68+
},
69+
baz: {
70+
type: Array,
71+
required: true
72+
}
73+
},
74+
emits: {
75+
foo: () => {},
76+
bar: null
77+
}
78+
})
79+
80+
expectType<{
81+
foo?: string
82+
bar: number
83+
baz: unknown[]
84+
}>(props)
85+
86+
props.foo && props.foo + 'bar'
87+
props.bar + 1
88+
// @ts-expect-error should be readonly
89+
props.bar++
90+
props.baz.push(1)
91+
92+
emit('foo')
93+
emit('bar')
94+
// @ts-expect-error
95+
emit('baz')
96+
})

0 commit comments

Comments
 (0)