Fonctionnement
Plugins intéréssants
Recettes
Changelog
Webpack 4
15 min

Comme vous le devinez je suis un grand fan du TypeScript car cela me permet de capturer pas mal d'erreurs en amont. Aussi, lorsque je travaille avec VueJS je continue à utiliser ce langage. Je vous propose de partager avec vous les configurations possibles pour utiliser TypeScript et VueJS avec webpack.

.vue

La première méthode consiste à rajouter le loader typescript tout en continuant à utiliser les .vue.

npm i -d vue-loader ts-loader typescript

Ensuite on ajoute la configuration typescript avec un fichier tsconfig.json

{
  "include": ["./src/**/*"],
  "compilerOptions": {
    "outDir": "./dist/",
    "sourceMap": true,
    "strict": true,
    "module": "es2015",
    "moduleResolution": "node",
    "target": "es5",
    "experimentalDecorators": true
  }
}

Et si on veut inclure des .vue depuis un fichier .ts il faut rajouter un peu de configuration au niveau du ts-loader.

{
  test: /\.ts$/,
  loader: 'ts-loader',
  options: {
    appendTsSuffixTo: [/\.vue$/]
  }
}

Enfin vos fichiers .vue vont s'écrire légèrement différemment, en utilisant Vue.extend() afin de permettre au Typescript de mieux comprendre ce que vous exportez.

<template>
    <div>
        <div class="greeting">Bonjour {{name}} {{exclamationMarks}}</div>
        <button @click="decrement">-</button>
        <button @click="increment">+</button>
    </div>
</template>

<script lang="ts">
import Vue from "vue";
export default Vue.extend({
    props: ['name', 'initialEnthusiasm'],
    data() {
        return {
            enthusiasm: this.initialEnthusiasm,
        }
    },
    methods: {
        increment() { this.enthusiasm++; },
        decrement() {
            if (this.enthusiasm > 1) {
                this.enthusiasm--;
            }
        },
    },
    computed: {
        exclamationMarks(): string {
            return Array(this.enthusiasm + 1).join('!');
        }
    }
});
</script>

.vue et décorateurs

Il est aussi possible de bénéficier des fonctionnalités avancées du TypeScript, comme les décorateurs, pour créer des composants basés sur les classes, plutôt qu'un simple objet. On commence par installer les décorateurs nécessaires :

npm i -D vue-property-decorator

Et le code de notre composant peut maintenant s'écrire plus simplement :

<template>
    <div>
        <div class="greeting">Bonjour {{name}}{{exclamationMarks}}</div>
        <button @click="decrement">-</button>
        <button @click="increment">+</button>
    </div>
</template>

<script lang="ts">
import { Vue, Component, Prop } from "vue-property-decorator";

@Component
export default class Hello extends Vue {
    @Prop({type: String}) name: string;
    @Prop({default: 0, type: Number}) initialEnthusiasm: number;

    enthusiasm = this.initialEnthusiasm;

    increment() {
        this.enthusiasm++;
    }
    decrement() {
        if (this.enthusiasm > 1) {
            this.enthusiasm--;
        }
    }

    get exclamationMarks(): string {
        return Array(this.enthusiasm + 1).join('!');
    }
}
</script>

Cette méthode est à mon goût plus intéréssante et plus naturelle que la première option.

.ts et template en "string"

Personnellement, je ne suis pas un très grand fan des extensions .vue car elle nécessite de configurer les éditeurs/IDE d'une certaines manière mais aussi de configurer les outils tiers pour les aider à comprendre où se trouve le typescript dans le code (tslint par exemple). Aussi, je préfère utiliser simplement des .ts.

En reprenant l'exemple précédent il est possible d'écrire le composant de cette manière :

import { Vue, Component, Prop } from "vue-property-decorator";

@Component({
  template: `
    <div>
        <div class="greeting">Bonjour {{name}}{{exclamationMarks}}</div>
        <button @click="decrement">-</button>
        <button @click="increment">+</button>
    </div>
  `
})
export default class Hello extends Vue {
    @Prop() name: string;
    @Prop() initialEnthusiasm: number;

    enthusiasm = this.initialEnthusiasm;

    increment() {
        this.enthusiasm++;
    }
    decrement() {
        if (this.enthusiasm > 1) {
            this.enthusiasm--;
        }
    }

    get exclamationMarks(): string {
        return Array(this.enthusiasm + 1).join('!');
    }
}

Le problème est alors que l'on perd la conversion du template en virtual dom. Heureusement, on peut faire la conversion avec le loader ts-vue-loader qui va transformer les chaines template à la volée :

npm i -D ts-vue-loader

Et dans la configuration webpack

{
  test: /\.ts$/,
  use: [{
    loader: 'ts-vue-loader'
  }, {
    loader: 'ts-loader',
    options: {
      appendTsSuffixTo: [/\.vue$/]
    }
  }]
}

Le loader doit être placé avant le loader ts-loader car il agit sur le code "compilé" par TypeScript. Avec ce loader vous pouvez maintenant vous passer des .vue et écrire vos composants avec tu TypeScript pur.

.ts et template séparé

Ecrire le template sous forme de simple chaine de caractère peut suffire pour des templates simples, mais si vous avez plusieurs dixaines de lignes d'HTML cela n'est pas vraiment pratique. Il est alors possible de séparer l'HTML dans son propre fichier.

import { Vue, Component, Prop } from "vue-property-decorator";
import withRender from './Hello.html'

@withRender
@Component
export default class Hello extends Vue {
    @Prop() name: string;
    @Prop() initialEnthusiasm: number;

    enthusiasm = this.initialEnthusiasm;

    increment() {
        this.enthusiasm++;
    }
    decrement() {
        if (this.enthusiasm > 1) {
            this.enthusiasm--;
        }
    }

    get exclamationMarks(): string {
        return Array(this.enthusiasm + 1).join('!');
    }
}

Dans ce cas là il faut charger un nouveau loader qui va permettre de convertir les .html en décorateurs

{
  test: /\.html$/, 
  use: 'vue-template-loader'
}

Et ajouter un fichier de déclaration pour aider le typescript à comprendre ce que retourne un .html.

declare module '*.html' {
  import Vue, { ComponentOptions } from 'vue'
  interface WithRender {
    <V extends Vue>(options: ComponentOptions<V>): ComponentOptions<V>
    <V extends typeof Vue>(component: V): V
  }
  const withRender: WithRender
  export default withRender
}

Et voila ! Cette seconde option offre l'avantage de permettre un rechargement à chaud du template sans perdre l'état du composant lorsque vous changez le code du .html.

Tout se recharge à chaque modif !

Petite aparté concernant un "problème" rencontré quand on mélange .ts et hot reload. Lorsque l'on modifie un composant il semble vouloir recharger les composants parents. D'après mes expérimentations, le problème vient du Typechecker qui repasse sur tous les fichiers. Pour éviter ce problème, mais aussi pour améliorer les performances je vous conseille d'utiliser le plugin fork-ts-checker-webpack-plugin qui permet de réaliser le typechecking sur un process séparé.

const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');

let config = {
  // ...
  module: {
    rules: [{
      test: /\.ts$/,
      use: [{
        loader: 'ts-vue-loader'
      }, {
        loader: 'ts-loader',
        options: {
          transpileOnly: true,
          appendTsSuffixTo: [/\.vue$/]
        }
      }]
    },
      {test: /\.html$/, use: 'vue-template-loader'}
    ]
  },
  plugins: [
    new ForkTsCheckerWebpackPlugin(),
    new webpack.DefinePlugin({
      'process.env': {
        'NODE_ENV': JSON.stringify(process.env.NODE_ENV)
      }
    })
  ]
  // ... 
}