]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
wip: type inference for useOptions
authorEvan You <yyx990803@gmail.com>
Fri, 13 Nov 2020 05:01:44 +0000 (00:01 -0500)
committerEvan You <yyx990803@gmail.com>
Fri, 13 Nov 2020 05:01:44 +0000 (00:01 -0500)
packages/runtime-core/src/helpers/useOptions.ts
test-dts/useOptions.test-d.ts [new file with mode: 0644]

index 172b2aaad1803d689e777ff7c89ba87396a8b8fb..055f2807845d4a7cf69b0c257909d55e64accf8a 100644 (file)
@@ -1,22 +1,88 @@
+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 ` +
@@ -24,5 +90,5 @@ export function useOptions<T extends Partial<DefaultContext> = {}>(
         `and should not be used in final distributed code.`
     )
   }
-  return null as any
+  return 0 as any
 }
diff --git a/test-dts/useOptions.test-d.ts b/test-dts/useOptions.test-d.ts
new file mode 100644 (file)
index 0000000..8330c53
--- /dev/null
@@ -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')
+})