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 { 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 | ||||
| } | ||||
|  | ||||
							
								
								
									
										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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user