Telerik blogs
TB 1200x303 Blog Cover

I am sure you have already seen that defineComponent is the key word for TypeScript in Vue 3—but do you know how it works? Let’s take a peek behind the curtain and see where the magic happens!

Imagine the situation: It is the end of a long coding day and all your logic is working perfectly—like a brand-new Ferrari with a V12 engine and only 4 miles on it. But, there’s still that one nasty TypeScript error that doesn’t want to go away, and it’s breaking the build no matter what you try! I’ve been there, and if you’ve felt this way too, please accept this hug from me.

i-feel-you meme two hugging

TypeScript support in Vue 2 was not good enough to use—there were many missing cases and many hacky workarounds. In Vue 3, on the other hand, we have the amazing defineComponent method that makes it very easy to move existing projects to TypeScript—so easy that even your cat could do it for you.

cat in glasses sitting in front of a computer

To TypeScript or Not?

Whether or not you like TypeScript, I’m going to try to convince you that it is one of the greatest new features in Vue 3. Its superpower is to save you time by catching errors and providing fixes before you run code. This actually means that you test while coding.

It also makes it possible to instantly use lots of tools that can increase productivity, like VueDX, Vetur or the amazing Volar for example. Once you set it up, you will be able to use all kind of autocompletions of modules, global components configurations and quick type fixes, and all this will be achieved without the need to even build our project.

I will try to reveal all the information I found about TypeScript in Vue 3 in the lines below—so you can feel much more familiar with it and can think of it as an option when starting a new Vue CLI project.

What Is The Main Idea Behind It?

If you peek at the official Vue 3 documentation, you will notice that there plenty of examples that explain how to use TypeScript in many different scenarios. The coolest part is that in order to have all this working, you don’t have to remember to include different kinds of interfaces for the component. All you have to do is wrap the object of the component setting in a defineComponent function. Having this option provides the ability to instantly embrace TypeScript in even larger codebase projects.

This is exactly the approach that keeps proving that Vue is the easiest JavaScript framework to get started with, isn’t it?

What Would We Do Without 'defineComponent'?

If we had to add the types without the defineComponent function, we would need to add a complicated combination of interfaces before each of our components depending on its content, as it is done in the imaginary example in the code below:

const myComponent: DefineComponent<
  PropsOrPropOptions = {},
  RawBindings = {},
  D = {},
  C extends ComputedOptions = ComputedOptions,
  M extends MethodOptions = MethodOptions,
  Mixin extends ComponentOptionsMixin = ComponentOptionsMixin,
  Extends extends ComponentOptionsMixin = ComponentOptionsMixin,
  E extends EmitsOptions = Record<string, any>,
  EE extends string = string,
  PP = PublicProps,
  Props = Readonly<ExtractPropTypes<PropsOrPropOptions>>,
  Defaults = ExtractDefaultPropTypes<PropsOrPropOptions>
= {
}

That was not pretty code for everyday usage, was it? That exactly is one of the reasons why the anti-TypeScript community exists. Not only that, but we would always have to choose a different interface declaration for each component based on the following five significant Vue declaration cases. If it is:

  1. A setup function
  2. An object format with no props
  3. An object format with array of props
  4. An object format with object props
  5. A custom option definition

That would be a horrible experience, wouldn’t it!? And that is exactly why defineComponent is here!

How Is It Implemented?

The concepts behind defineComponent bring us back to the roots of JavaScript—the pure functions and TypeScript function overloading. To make it easy for users, all the logic is hidden behind one name: defineComponent.

We always pass any object and return the correctly typed object. All the cases for different objects that could create a Vue component are handled in separate overloads. The exact code for its implementation could be found in the GitHub repository here, but I’ll try to explain each overload below:

If you are not familiar with function overloads, think of them as many functions with the same name, just different parameters (by number or type) that still have the same return type.

The first overload is dealing with the case when we create the component from a setup function:

// overload 1: direct setup function
defineComponent({
    setup (props, ctx) {
    return {…}
}
});

The second one is the case when the props are defined as an empty object:

// overload 2: object format with no props
defineComponent({
    props: {},
    
});

The third one is the case when the props of the component are defined as array of strings and defineComponent is expecting props as an interface of a collection—Readonly<{ [key in PropNames]?: any }>. Here is how it should look:

// overload 3: object format with array props declaration
defineComponent({
props: ['postTitle'],
});

The fourth one is the case when the props are defined as an array of strings and the function is expecting props of type PropsOptions extends Readonly<ComponentPropsOptions>, as shown below:

overload 4: object format with object props declaration
defineComponent({
props: {
  title: String,
  likes: Number
}

The “last but not the least” option is to pass an object or type unknown or an object with defined setup and name:

// implementation, close to no-op
defineComponent({
setup: function,
name: ‘some name’
});

As you see, all these cases are responsible for different scenarios, yet the return type is always the same: a defineComponent interface with all the types you need to apply for the props, data and the other settings that are needed for the component. Yet another piece of evidence that the best solutions are usually the simplest ones.

Deeper Precision

Using defineComponent is extremely helpful and allows you to grab your project and join the dark forces of TypeScript world—yet if you want to have correct types on all the properties that could be used in the component, you may need to add a bit more love.

Below is the cheat sheet piece of code that I personally use to peek from time to time at how to add types correctly in a Vue component:

export default defineComponent({
  name: 'TypescriptExamples',
  props: {
    name: {
      type: String,
      required: true
    },
    id: [Number, String],
    success: { type: String },
    callback: {
      type: Function as PropType<() => void>
    },
    book: {
      type: Object as PropType<Book>,
      required: true
    },
    metadata: {
      type: null // metadata is typed as any
    },
    bookA: {
      type: Object as PropType<Book>,
      // Make sure to use arrow functions
      default: () => ({
        title: 'Arrow Function Expression'
      }),
      validator: (book: Book) => !!book.title
    },
    bookB: {
      type: Object as PropType<Book>,
      // Or provide an explicit this parameter
      default(this: void) {
        return {
          title: 'Function Expression'
        }
      },
      validator(this: void, book: Book) {
        return !!book.title
      }
    }
  },
  setup(props) {
    const result = props.name.split('') // correct, 'name' is typed as a string
    const year = ref(2020)
    const yearsplit = year.value.split('')// => Property 'split' does not exist on type 'number'
    const stringNumberYear = ref<string | number>('2020') // year's type: Ref<string | number>
 
    stringNumberYear.value = 2020 // ok!
    const modal = ref<InstanceType<typeof MyModal>>()
    const openModal = () => {
      modal.value?.open()
    }
    const book = reactive<Book>({ title: 'Vue 3 Guide' })
    // or
    const book1: Book = reactive({ title: 'Vue 3 Guide' })
    // or
    const book2 = reactive({ title: 'Vue 3 Guide' }) as Book
 
    const handleChange = (evt: Event) => {
      console.log((evt.target as HTMLInputElement).value)
    }
    return {
      modal, openModal,
      book, book1, book2,
      handleChange
    };
},
  emits: {
    addBook(payload: { bookName: string }) {
      // perform runtime validation
      return payload.bookName.length > 0
    }
  },
  methods: {
    onSubmit() {
      this.$emit('addBook', {
        bookName: '123'
     //   bookName: 123 // Type error!
      })
 
    //  this.$emit('non-declared-event') // Type error!
    }
  }

Writing Tests With Typescript like a Pro

My favorite part about using defineComponent and TypeScript in Vue 3 is adding tests to the project. It is a true game changer in that area and even makes the process delightful because it provides those awesome autocompletions that help cover all the use cases when writing tests.

Even better, TypeScript also reduces the overall number of tests that are needed because the types are checking most of the possible configurations statically.

Here is an example how a simple test-writing process would look like when we want to test a simple Vue 3 component with a Kendo Native Grid in it.

testtypescript

And here is the code:

it('Kendo Grid renders 1 item', () => {
   const wrapper1 = mount(Grid, {
     props: {
         dataItems: [{
           Id:1,
           Product: 'toy'
         }]
      },
       
   })
   expect(wrapper1.findAll('.k-master-row').length).toBe(1)
})
 
it('Kendo Grid renders 2 items', () => {
   const wrapper1 = mount(Grid, {
     props: {
         dataItems: [{
           Id:1,
           Product: 'toy'
         },
         {
           Id:2,
           Product: 'car'
         }]
      },
       
   })
   expect(wrapper1.findAll('.k-master-row').length).toBe(2)
})

You can check other similar examples with all the Kendo UI for Vue components in our repository here.

My next blog will cover state management techniques when editing Grid items, so stay tuned for that.

Will You Use TypeScript in Vue?

Now that you have read all this, you can decide if using TypeScript in your current Vue project is right for you. Personally, I already know I’m going for it!

Thanks for reading so far! For more Vue tips or Kendo UI for Vue news, follow me on Twitter at @pa4oZdravkov.

Happy TypeScript and Vue coding!


Plamen Zdravkov
About the Author

Plamen Zdravkov

Plamen Zdravkov (@pa4oZdravkov) is a Principle Software Engineer for Kendo UI at Progress and is into the art of web development for over a decade now. He loves working with JavaScript and .NET web technologies and through the years took active part in the evolution of the Telerik ASP.NET AJAX, Kendo UI for jQuery and ASP.NET MVC component libraries—first as a Support Officer and later as a developer. Nowadays he leads the development of Kendo UI for Vue and Telerik UI for ASP.NET Core component libraries.

Related Posts

Comments

Comments are disabled in preview mode.