Home > Back-end >  JavaScript component conversion to TypeScript: What is the type of props
JavaScript component conversion to TypeScript: What is the type of props

Time:10-23

Migrating VueJS project from JavaScript Options API to TypeScript Composition API, I have gradually found equivalents of most of the stuff. One thing that I'm struggling with is the v-model feature. I found a good article on implementing it using Composition API, where author creates a composable function that can be reused in components that want to implement v-model. I'm trying to now write an equivalent function using TypeScript.

Here is the original JS code:

import { computed } from 'vue'
export function useModelWrapper(props, emit, name = 'modelValue') { 
  return computed({ 
    get: () => props[name], 
    set: (value) => emit(`update:${name}`, value) 
  })
}

My TS implementation looks like this:

import { computed, ComputedRef } from '@vue/composition-api'

export function useModelWrapper<T> (props: Record<string, unknown>, emit: (event: string, value: T) => void, name: 'modelValue') : ComputedRef {
  return computed({
    get: () => props[name],
    set: (value) => emit(`update:${name}`, <T>value)
  })
}

This compiles fine but Vue is not too happy about it. I get

No overload matches this call

error if I use this function in a component.

My suspicion is that the type of props in the TS version is not correct, but I was not able to figure out what type I should use there. I have tried it with Object, unknown and any, but none of them allows me to do props[name] in the getter (any does but TS complains that I shouldn't use any as the type of props).

What am I doing wrong here?

Edit

Here is the full error text:

{
    "resource": "../ProjectsDropdown.vue",
    "owner": "_generated_diagnostic_collection_name_#6",
    "code": "2769",
    "severity": 8,
    "message": "No overload matches this call.
Overload 1 of 3, '(options: ComponentOptionsWithoutProps<unknown, unknown, Data, {}, {}>): VueProxy<unknown, unknown, Data, {}, {}>', gave the following error.
Type '{ modelValue: PropType<Project>; }' is not assignable to type 'undefined'.
Overload 2 of 3, '(options: ComponentOptionsWithArrayProps<string, Data, Data, {}, {}, Readonly<{ [x: string]: any; }>>): VueProxy<Readonly<{ [x: string]: any; }>, Data, Data, {}, {}>', gave the following error.
Type '{ modelValue: PropType<Project>; }' is not assignable to type 'string[]'.
  Object literal may only specify known properties, and 'modelValue' does not exist in type 'string[]'.
Overload 3 of 3, '(options: ComponentOptionsWithProps<ComponentPropsOptions<Data>, Data, Data, {}, {}, ({ [x: number]: string; } & { [iterator]?: IterableIterator<string> | undefined; ... 32 more ...; 
toLocaleString?: string | undefined; }) | ({} & { ...; })>): VueProxy<...>', gave the following error.
Type '{ modelValue: PropType<Project>; }' is not assignable to type 'ComponentPropsOptions<Data> | undefined'.
  Types of property 'modelValue' are incompatible.
    Type 'PropType<Project>' is not assignable to type 'Prop<unknown, unknown> | null | undefined'.
      Type 'new (...args: never[]) => Project & object' is not assignable to type 'Prop<unknown, unknown> | null | undefined'.
        Type 'new (...args: never[]) => Project & object' is not assignable to type 'new (...args: string[]) => Function'.
          Types of parameters 'args' and 'args' are incompatible.
            Type 'string' is not assignable to type 'never'.",
    "source": "Vetur",
    "startLineNumber": 59,
    "startColumn": 16,
    "endLineNumber": 119,
    "endColumn": 3
}

Edit 2

Here is how I'm using this function in a component:

<script lang='ts'>
export default defineComponent({
  ...
  import { useModelWrapper } from '../usemodelWrapper'
  props: {
    modelValue: Object as PropType<Project>
  },
  setup (props, { emit }) {
  return {
    selectedProject: useModelWrapper<Project>(props, emit, 'modelValue')
  }
})
</script>

CodePudding user response:

Just in case it helps someone down the road, it turned out that I was using wrong version of PropType in the component to define my v-model prop. The real PropType class should be imported like this:

import { PropType } from '@vue/composition-api'

and not:

import Vue, { PropType } from 'vue'

Edit

If it helps anyone, here is how I finally implemented a strongly-typed v-model using Composition API and TypeScript:

import { computed, WritableComputedRef } from '@vue/composition-api'

export interface IIndexable<T> { [key: string]: T }

export function useModelWrapper<T> (props: IIndexable<T>, emit: (event: string, value: T) => void, name = 'modelValue') : WritableComputedRef<T> {
  return computed<T>({
    get: () => props[name] as T,
    set: (value: T) => emit('input', value as T)
  })
}

You can then import it in any component like this:

export default defineComponent({
  name: 'ProjectsDropdown',
  props: {
    value: Object as PropType<Project>
  },
  setup (props, { emit }) {
    const selectedProject = useModelWrapper<Project>(props, emit, 'value')

    return { selectedProject }
  }
}

and bind it to whatever you want to bind in your component's template.

Please note that this works in Vue 2.x. For Vue 3, you'll want to change the name of value prop to modelValue and emit('input', value as T) with emit('update:modelValue', value) in the above code.

Edit 2

See @emeraldsanto's answer below. It provides an even better implementation.

CodePudding user response:

After seeing your own answer, I feel like this could be even stricter.

import { computed, WritableComputedRef } from '@vue/composition-api'

export function useModelWrapper<TProps, TKey extends keyof TProps> (
  props: TProps,
  emit: (event: string, value: TProps[TKey]) => void,
  name: TKey = 'modelValue' as TKey
) : WritableComputedRef<TProps[TKey]> {
  return computed<TProps[TKey]>({
    get: () => props[name],
    set: (value: TProps[TKey]) => emit('input', value)
  })
}

I haven't tested it with Vue specifically but that function should restrict which key you can pass it as third argument and correctly infer the return type depending on which name was passed instead of a union of all values of props.

  • Related