Tips using Typescript and Vue

Tips using Typescript and Vue

Typescript is a great “language”, makes it possible to create more maintainable and understandable software, but requires extra effort to type the variables, the functions’ arguments, etc…

Vue 2.x, and even more Vue 3 provide a great typescript integration, providing the necessary types to use your app, but not always are trivial, and you need to know the types you must use in every case.

I want to share with all of you the lessons I learned in my experience using Vue and TS, the typical questions, and the “problems” I found in the way.

Vuex

Typing the Vuex’s store can’t be straightforward, my first time typing the store was frustrating because I didn’t know types use.

State

The state is a JS object, in type you can type it as a generic Record<string, any> but this is not nice. It’s better creating and interface that define all the store items types, for example, imagine this store:

const store = {
  name: 'Sergio',
  lastLogin: new Date(2021, 0, 1, 22, 34),
  config: {
    darkTheme: true,
    fontSize: 23
  },
  friends: [{ id: 1, name: 'Juan' }, { id: 2, name: 'Felipe' }]
}

We must create an interface for this object:

interface Friend {
  id: number;
  name: string;
}
interface StoreState {
  name: string;
  lastLogin?: Date,
  config: {
    darkTheme: boolean;
    fontSize: number;
  },
  friends: Friend[]
}
const store: StoreState = {
  name: 'Sergio',
  lastLogin: new Date(2021, 0, 1, 22, 34),
  config: {
    darkTheme: true,
    fontSize: 23
  },
  friends: [{ id: 1, name: 'Juan' }, { id: 2, name: 'Felipe' }]
}

Mutations

For the mutations, Vuex provides the type MutationTree<S>, defined as:

interface MutationTree<S> {
    [key: string]: Mutation<S>;
}
type Mutation<S> = (state: S, payload?: any) => any;

Basically is a map of mutation functions, as you can see, a mutation function get the state type, but the payload can be anything and return anything

const mutations: MutationTree<StoreState> = {
    setName (store, payload: string) {
        store.name = payload
    }
}

As the payload is defined by the type as any it’s a good practice type your payload in every mutation function

Actions

It’s similar to the mutations, but with a peculiarity:

interface ActionTree<S, R> {
    [key: string]: Mutation<S, R>;
}
type Action<S, R> = ActionHandler<S, R> | ActionObject<S, R>;

Without going deeper, the S is the state of the vuex module, and R is the Root State. In a simple case (without using vuex modules) S and R are the same.

Getters

Same as actions,

interface GetterTree<S, R> {
    [key: string]: Getter<S, R>;
}

For example:

const getters: GetterTree<StoreState, StoreState> = {
    friendCount(store): number {
      return store.friends.length
    }
}

As in the store payload params, it’s a good practice to type getter return

Composition API

If you are using composition API in the setup function we can type our properties as we did in the store. Make sure you are using defineComponent instead Vue.extend to make it work

interface Props {
    value: boolean,
    title: string    
}

export default defineComponent({
    name: 'my-component',
    props: {
        value: Boolean,
        title: String        
    },
    setup (props: Props) {
     ...
    }
})

You can also type the properties directly in the props entry, but as typescript interfaces don’t exist at runtime we can’t use the interface directly as the property type

// Doesn't work because Friend doesn't exists in the runtime
{
  props: {
    friend: {
      type: Friend
    }
  }
}
// Doesn't work because Object doesn't implement Friend properties
{
  props: {
    friend: {
      type: Object as Friend
    }
  }
}

But, we can pass the type as return of a function, then Vue instance the interface instances the interface and can check the value type

// Works
{
  props: {
    friend: Object as () => Friend,
    friends: Array as () => Friend[],
    name: String as () => string
  }
}

Read more about that

Remember to type “native” types because String is not the same as string (String is an object and string is a type) More info about this in Stackoverflow

Add extra properties to Vue Component Object

By default, Vue provides us a defined structure for the Vue Component Object, for example, the property data, props, etc… Using vanilla JS we can add a new property to the Vue Component Object without doing extra works, for example, we want to add a property called layout that makes our root component can use different layouts in our view.

export default {
    name: 'my-component',
    layout: '2-cols' 
}

If we try to do this using typescript we will get an error because the property layout wasn’t defined in the Vue Component Object. To fix it we must extend the definition creating a definition file in our src/, for example, src/typings.d.ts

# src/typings.d.ts
import Vue from 'vue'

declare module 'vue/types/options' {
  interface ComponentOptions<V extends Vue> {
    layout?: string;    
  }
}

Add extra properties to the Vue Instance

As in the previous chapter, we could want to add a new property to the Vue Instance, for example, to add a global functionality like a toast, etc: vm.$toast.open()

Remember you can do it doing something like this, for example, during the plugin installation:

Vue.prototype.$toast = {
    open: () => {
        ....
    }
} as ToastHandler

Then we must add to our definition file these lines to declare the new Vue instance properties and their types

# src/typings.d.ts
import Vue from 'vue'
declare module 'vue/types/vue' {
  interface Vue {
    $toast: ToastHandler;
  }
}

Typescript can be tough at the beginning, but gives you more confidence in your code and make it more readable, for example

{
    props: {
        friend: Object as () => Friend,
        person: Object
    }
}

For example in the case of friend you only need to go to the type declarations to know the ‘friendstructure and properties, even your IDE can provide you autocomplete, but forperson` is very hard to know the object structure. I hope this post can help you using Typescript and Vue.