# Vue + Typescript
Przykładowe repozytorium: https://git.macopedia.pl/frontend/workshop/vue-ts/-/tree/master
Projekt stworzony za pomocą Vue CLI + wbrane pakiety TS.
# Przygotowanie IDE
Niepodważalną zaletą TS są adnotacje oraz podpowiedzi. Aby móc ich używać przy pracy z Vue potrzebujemy odpowiedniego IDE. My wybieramy VSC + wtyczka Vetur. Wymagane jest ustawienie dotyczące interpolacji: https://vuejs.github.io/vetur/guide/setup.html#advanced
module.exports = {
settings: {
"vetur.experimental.templateInterpolationService": true
}
}
2
3
4
5
# Konfiguracja
Aby transpilator poprawnie interpretował pliki .vue musimy zdefiniować czym one są. Robimy to w plikach shims-vue.d.ts oraz shims-tsx.d.ts.
W ten sposób informujemy transpilator, że kod importowany z plików .vue będzie instancją klasy Vue.
declare module '*.vue' {
import Vue from 'vue'
export default Vue
}
2
3
4
# Vue.extend zamiast export default
Aby użyć TypeScript z vue2 mamy dwie opcje:
- Class API - bardziej naturalny zapis w kontekście vanilla js (https://class-component.vuejs.org/)
- Options API - bardziej natraulny zapis w kontekście componentów Vue
My wybieramy opcję drugą, ze względu na spójny zapis komponentów Vue oraz dlatego, że class API jest dodatkową biblioteką.
Jak więc napisać pierwszy komponent Vue w TypeScript wraz z Options API ?
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({
name: 'PersonCard',
props: {
person: {
type: Object
required: false
}
},
});
</script>
2
3
4
5
6
7
8
9
10
11
12
13
Na powyższym przykładzie widzimy:
- lang=ts - informujemy vue-loader, aby interpretował nasz kod jako typescript
- zamiast export default używamy Vue.extend do definicji komponentu, w ten sposób transpilator ts wie z czym będzie miał do czynienia - za pomocą Vue.extend tworzymy subklasę konstruktura Vue - jako argument podajemy component options, w ten sam sposób jak byśmy używali Vue bez TS.
# Adnotacje propsów
Vue ma bardzo prosty walidator propsów - możemy zdefiniować czy dana wartość jest obiektem czy stringiem. Jeżeli chcemy zdefiniować bardziej złożony interfejs możemy zrobić to za pomocą PropType https://vuejs.org/v2/guide/typescript.html#Annotating-Props.
Definicja typu propsa wygląda bardzo podobnie - jedyne co musimy zrobić to użyć as PropType<T> - gdzie PropType jest typem generycznym, do którego możemy przekazać nasz interfejs.
<template>
<div class="hello">
{{ person.name }}
</div>
</template>
<script lang="ts">
import Vue, { PropType } from 'vue';
export default Vue.extend({
name: 'PersonCard',
props: {
person: {
type: Object as PropType<{
name: string,
lastname: string,
nick?: string
}>,
required: true
}
},
});
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Data
Inferencja typów w data działa dobrze i większości nie będziemy musieli dodatkowo opisywać typów. Jeżeli chcemy możemy to zrobić za pomocą operatora as:
<script lang="ts">
import Vue, { PropType } from 'vue';
import type { Person } from './Person.types'
export default Vue.extend({
data () {
return {
id: 0 as number
}
},
props: {
person: {
type: Object as PropType<Person>,
required: true
}
}
});
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# Computed
Podczas pracy z vue2+ts możemy napotkać błedy niezwiązane z prawdą - np. transpilator pogubi się kiedy nie otypujemy zwracanego typu dla wartości computed. Możemy zrobić to w ten sposób:
<script lang="ts">
import Vue, { PropType } from 'vue';
import type { Person } from './Person.types'
export default Vue.extend({
data () {
return {
id: 0 as number
}
},
props: {
person: {
type: Object as PropType<Person>,
required: true
}
},
computed: {
getPersonId (): string {
return `${this.person.name}-${this.id}`
}
},
});
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Rozwiązywanie problemów przy pracy z refsami
Praca z refsami jest utrudniona z powodu braku rozumienia przez TS cykli życia komponentu Vue (TS zawsze uważa, że pod refsem może być undefined), oraz przez typ zdefiniowany jako wartość refsów na instancji Vue. Problemy te można łatwo ograć poprzez wyciągnięcie refsa do zmiennej i podanie wprost jaki jest oczekiwany przez nas typ, lub wykonanie tego samego w locie
methods: {
getWidth() {
// type assertion na zmiennej
const header = this.$refs.header as HTMLElement
this.width = header.offsetWidth
// type assetion w locie
this.width2 = (this.$refs.header as HTMLElement).offsetWidth
}
}
2
3
4
5
6
7
8
9
10
# Rozwiązywanie problemów przy pracy z mixinami
Pracując z mixinami przy wykorzystaniu options API należy pamiętać, że wsparcie typescriptu wymaga zdefiniowania mixin nieco w inny sposób niż ma to miejsce w klasycznym vue2.
Opcje mixiny powinny rozszerzać instancję Vue
import Vue from 'vue'
export default Vue.extend({
methods: {
logClicked() {
console.log('clicked')
}
}
})
2
3
4
5
6
7
8
Tak zdefiniowaną mixinę należy zaimportować do komponentu gdzie ma zostać wykorzystana i rozszerzyć ją o zestaw opcji jakie ma posiadać komponent:
import ClickedMixin from '../mixins/ClickedMixin'
export default ClickedMixin.extend({
name: 'Mixin',
created() {
this.fetch()
}
})
2
3
4
5
6
7
8
W przypadku gdy chcemy skorzystać z większej ilości mixin warto skorzystać z pomocniczej paczki vue-typed-mixins, która pozwoli nam w łatwy sposób pałączyć udostępniane przez mixiny funkcjonalności, a następnie je rozszerzyć.
import ClickedMixin from '../mixins/ClickedMixin'
import FetchDataMixin from '../mixins/FetchDataMixin'
import mixins from 'vue-typed-mixins'
export default mixins(ClickedMixin, FetchDataMixin).extend({
name: 'Mixin',
created() {
this.fetch()
}
})
2
3
4
5
6
7
8
9
10
# Praca z zewnętrznymi rzeczami
Często będziemy korzystać z bibliotek, które TS nie używają lub nie dostarczają zdefiniowanych typów w swoim repozytorium. W tym przypadku musimy zdefiniować je sami.
np. wtyczka vue-mq - ktora nie dostarcza typów - bo jej zaimportowaniu otrzymamy błąd:
import VueMq from 'vue-mq'`
could not find a declaration file for module 'vue-mq'. '/Users/merc/Projects/Poligon/vue-ts/node_modules/vue-mq/dist/vue-mq.js' implicitly has an 'any' type.
Try `npm i --save-dev @types/vue-mq` if it exists or add a new declaration (.d.ts) file containing `declare module 'vue-mq';`ts(7016)
2
3
4
5
Aby blędy się pozbyć musimy dostarczyć deklaracje typów. Robimy to w pliku shims-vue.d.ts
declare module 'vue-mq' {
import {PluginObject} from 'vue';
interface VueMq extends PluginObject<any> {
VueMq: VueMq;
}
const VueMq: VueMq;
export default VueMq;
}
2
3
4
5
6
7
8
9
10
W ten sposób deklarujemy, że moduł vue-mq zwraca PluginObject
# Globalne właściwości
Nie rzadko się zdarza, że my lub zewnętrzne wtyczki dodajemy globalne właściwości na instancji Vue. Np. wtyczki do translacji często udostępniają helpery typu $t('label to translate) - w tym przypadku $t jest globalną właściwośćią - nie poinformowanie transpilatora o tym fakcie będzie skutkować błędem.
Weźmy pod uwagę wcześniejszą wtyczkę vue-mq, która udostępnia globalny helper $mq do zczytania aktualnego breakpointu.
Podczas pracy z tą biblioteką najprawdopodobniej zobaczymy blą∂ typu:
Property '$mq' does not exist on type 'CombinedVueInstance<Vue, unknown, unknown, { breakpoint: string; }, Readonly<Record<never, any>>>'.Vetur(2339)
Dlatego poinformujemy transpilator jak ma rozumieć globalną właściwość $mq w kontekćie komponentów Vue.
Robimy to w pliku shims-vue.d.ts:
declare module '*.vue' {
import Vue from 'vue'
export default Vue
module 'vue/types/vue' {
interface Vue {
$mq: 'sm' | 'md' | 'lg' // Or keyof BreakpointRecord
}
}
}
2
3
4
5
6
7
8
9
10
← Accessibility Nuxt.js →