Vue.js 2 and Vuex 3 with TypeScript
Tags: vuex3, vue, vuex, typescript
Published: 2020-05-16 22:55:28

---
title: Vue.js 2 and Vuex 3 with TypeScript
published: true
date: 2020-05-16 22:50:24 UTC
tags: vuex-3,vuejs,vuex,typescript
canonical_url: 
---

#### บันทึกไว้สักหน่อยหลังจากใช้ TypeScript กับ Project ที่เขียนด้วย Vue 2 และ ช่วงแรกที่ใช้ Vuex แล้วรู้สึกว่ามันลำบากมาก เพราะประสบการณ์ TypeScript น้อยด้วย แล้วไปเจอคำแนะนำหนึ่งที่ดูแล้วมันน่าจะง่ายสุดสำหรับตอนนี้ ระหว่างรอ Vuex4 + Vue.js 3 ก็มาลองเขียนแบบนี้กันดูก่อนละกัน

![](https://cdn-images-1.medium.com/max/1024/1*VqGesTor6fLdEgJrnOV5IQ.png)

ใครมือใหม่ลองเข้าไปศึกษาพื้นฐานได้จาก Clips ของผมก่อน หรือ จะดูของท่านๆ อื่นๆ ก็ได้

{% youtube 9Icy6FJjkhs %}

{% youtube fbszbMWAQNQ %}

{% youtube JlQcesJs39A %}

### Create Vue.js project

```
vue create vuex-typescript
```

โดยเลือก แบบ Manually

![](https://cdn-images-1.medium.com/max/984/1*ztiO3C2zR8tuMW8NZ-0nDw.png)

หลังจากนั้นก็เลือก packages ที่จะใช้

![](https://cdn-images-1.medium.com/max/989/1*IohGAkIHUED2cidj0Efadw.png)

เราจะใช้หลักๆ ก็คือ TypeScript, Router และ Vuex

ถัดไปรูปแบบของ component ตรงนี้ผมชอบแบบ class-style ค่อนข้างเข้าใจง่ายกว่า

![](https://cdn-images-1.medium.com/max/990/1*uDHCN0BZ0I-0bqkhLvBpVA.png)

หลังจากนี้ก็เลือกถามถนัด

เมื่อเสร็จแล้วก็สามารถเขียน code ได้แล้วครับ

ตัวอย่าง Code สามารถ clone มาศึกษาได้ที่

[mrchoke/vuex-typescript](https://github.com/mrchoke/vuex-typescript)

[Demo](https://mrchoke.github.io/vuex-typescript/)

Code ที่ได้จาก Vue Cli จะมีตัวอย่างมาให้สองหน้า คือ Home และ About ซึ่งผมได้เปลี่ยนแปลงบางส่วนเพื่อให้เหมาะกับตัวอย่างที่จะกล่าวถึง

### ขอบเขตของตัวอย่าง

ตัวอย่างผมจะยกตัวอย่างโดยแบ่งเป็น 3 routes ดังนี้

- Home หน้าแรก → src/ views/Home.vue
- Add form สำหรับเพิ่ม record → src/views/Add.vue
- View สำหรับแสดงผล records ทั้งหมด → src/views/View.vue

โดยผมจะใช้ vue-router สำหรับจัดการหน้าต่างๆ และ vuex สำหรับการเก็บ state ของ records

### Vue Router

src/router/index.ts

```
import Vue from 'vue'
import VueRouter, { RouteConfig } from 'vue-router'
import Home from '../views/Home.vue'

Vue.use(VueRouter)

const routes: Array<RouteConfig> = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/add',
    name: 'Add',
    component: () => import(/\* webpackChunkName: "add" \*/ '../views/Add.vue')
  },
  {
    path: '/view',
    name: 'View',
    component: () => import(/\* webpackChunkName: "view" \*/ '../views/View.vue')
  }
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE\_URL,
  routes
})

export default router
```

### Types

src/type/index.ts

ผมสร้าง type ขึ้นมาใช้สำหรับ project นี้โดยเก็บไว้ที่ src/type/index.ts

```
export class Student {
  id: number
  firstname: string
  lastname: string
  age: number

  constructor() {
    this.id = 0
    this.firstname = ''
    this.lastname = ''
    this.age = 7
  }

  get fullname(): string {
    return `${this.firstname} ${this.lastname}`
  }
}

export type Students = Student[]

export interface RootState {
  students: Students
}
```

ซึ่งจะมีอยู่สาม types คือ

#### Class Student

จะเก็บข้อมูลของนักเรียนแต่ละคน จะประกอบไปด้วย

- id → number
- firstname → string
- lastname → string
- age → number
- fullname → getter → string

#### Type Students

ประกาศ Type ใหม่ให้เท่ากับ Array ของ Class Student ไว้เก็บ Record ทั้งหมดของนักเรียน

#### Interface RootState

เป็นโครงสร้างของ state ที่จะนำไปใช้ใน Vuex ซึ่งตัวอย่างผมมีแค่ตัวเดียวคือ students จะเป็น record ทั้งหมดของนักเรียนนั่นเอง

### Vuex

วิธีที่ผมจะสาธิตในบทความนี้ไม่ต้องลงอะไรเพิ่มเติมนอกจาก packages ที่จำเป็น เช่น vuex, typescript ซึ่งการเขียนจะล้อตาม source code ของ Vuex ต้นฉบับ ที่มีการประกาศ Type ไว้แล้วซึ่งสามารถเข้าไปดูได้ที่

[https://github.com/vuejs/vuex/blob/v3.4.0/types/index.d.ts](https://github.com/vuejs/vuex/blob/v3.4.0/types/index.d.ts)

#### ถ้าเราเขียน Vuex แบบปกติจะมีโครงสร้างแบบนี้

```
import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex);

export default new Vuex.Store({

state: {},
mutations: {},
actions: {},
modules: {}

});
```

ซึ่ง Property state จะเป็นหัวใจหลัก พอมาเขียน TypeScript เราก็ต้องกำกับ Type ให้ state หลักซึ่งใน Type ของ Vuex ใช้ชื่อ RootState ซึ่งก็สื่อดี จริงๆ จะใช้ชื่ออะไรก็ได้นะครับ ซึ่งผมได้ประกาศไว้แล้วจากตัวอย่างด้านบน

ต่อไปเราก็แก้ไข src/store/index.ts

```
import Vue from 'vue'
import Vuex, { StoreOptions } from 'vuex'
import { RootState, Student, Students } from '@/type'

Vue.use(Vuex)

const store: StoreOptions<RootState> = {
  state: {
    students: []
  },
  mutations: {
    UPDATE\_STUDENTS(state, student: Student) {
      state.students.push(student)
    },
    DELETE\_STUDENTS(state, id: number) {
      const search = state.students.filter(i => i.id !== id)
      state.students = search
    }
  },
  actions: {
    updateStudents(contex, student: Student) {
      contex.commit('UPDATE\_STUDENTS', student)
    },
    deleteStudents(contex, id: number) {
      contex.commit('DELETE\_STUDENTS', id)
    }
  },
  getters: {
    students(state): Students {
      return state.students
    },
    maxId(state): number {
      return state.students.reduce((max, student) => (student.id > max ? student.id : max), state.students[0]?.id ?? 0)
    },
    total(state): number {
      return state.students.length
    },
    latest(state): Student {
      return state.students.slice(-1)[0]
    }
  }
}
export default new Vuex.Store<RootState>(store)
```

ผมออกแบบตัวอย่างไว้คือ เราสามารถ เพิ่ม และ ลบ record ของนักเรียนได้ สามารถดึงจำนวน record ทั้งหมด ดึง record สุดท้าย และ ดึงค่า Max ID ได้

#### สร้าง Store

```
const store: StoreOptions<RootState> = {
  ...
}
```

โดยประกาศ type ให้กับ store เป็น StorageOptions และ ส่ง RootState เข้าไป หลังจากนั้นเราก็สามารถใส่ properties ต่างๆ ของ store เข้าไปได้ ตัว store หลักนี่ค่อนข้างง่ายไม่ซับซ้อนเหมือน module (จะยกตัวอย่างภายหลัง)

#### State

```
state: {
    students: []
}
```

การประกาศ state เราต้องประกาศให้ตรงกับ RootState นะครับเป็นอย่างอื่นไม่ได้เลย TypeScript จะโวยวายทันที

#### Mutations

```
mutations: {
    UPDATE\_STUDENTS(state, student: Student) {
      state.students.push(student)
    },
    DELETE\_STUDENTS(state, id: number) {
      const search = state.students.filter(i => i.id !== id)
      state.students = search
    }
}
```

จะมีสอง handler คือ

- UPDATE\_STUDENTS จะมี payload เป็น นักเรียนแต่ละคน type Student ที่สร้างไว้ก่อนหน้านี้ ซึ่งจะ push ค่าเข้าไปเก็บไว้ใน state students
- DELETE\_STUDENTS จะมี payload เป็นค่า id ของนักเรียน เมื่อรับมาแล้วจะทำการ filter id ตัวนี้ทิ้งไป แล้วปรับค่าของ state students ใหม่

#### Actions

```
actions: {
    updateStudents(contex, student: Student) {
      contex.commit('UPDATE\_STUDENTS', student)
    },
    deleteStudents(contex, id: number) {
      contex.commit('DELETE\_STUDENTS', id)
    }
}
```

actions จะคล้ายๆ กับ mutations แต่แทนที่จะทำตรงๆ ก็ทำการ commit ผ่าน mutations และ ถ้าใครจะ get/post api จะสามารถทำผ่าน actions ได้เพราะจะสามารถเรียกใช้ async/await ได้

ตัวอย่างผมมีสอง actions คือ

- updateStudents รับ payload Students มาแล้ว commit mutation
- deleteStudents รับ payload id มาแล้ว commit mutation

#### Getters

```
getters: {
    students(state): Students {
      return state.students
    },
    maxId(state): number {
      return state.students.reduce((max, student) => (student.id > max ? student.id : max), state.students[0]?.id ?? 0)
    },
    total(state): number {
      return state.students.length
    },
    latest(state): Student {
      return state.students.slice(-1)[0]
    }
  }
```

ปกติถ้าเขียนไม่ซับซ้อนมากนักเราสามารถเรียกค่าจาก state ตรงๆ ใน component ได้เลยแต่บางครั้งเราต้องทำการประมวลผลก่อน การที่จะทำผ่าน computed ของ component ซ้ำๆ กันหลายๆ ครั้งก็ไม่ค่อยสวยนัก ก็เรียกผ่าน getters จะสวยกว่า

ตัวอย่างผมจะทำการดึงค่า 4 ค่าไปใช้ดังนี้

- students ดึง records ทั้งหมดไปใช้จะเห็นว่าผม return state.students ไปเฉยๆ แบบนี้เราสามารถเรียกผ่าน computed ก็ได้เล่น  
computed: {  
 students () {  
 return this.$store.students  
 }  
}
- maxId ผมจะดึงค่า ID ล่าสุดไปใช้สำหรับสร้าง ID ใหม่
- total ดึงจำนวน records ทั้งหมดไปใช้งาน จริงๆ เราสามารถใช้ length ของ students ใน component ตรงๆ ก็ได้
- latest ผมดึง record ล่าสุดไปแสดงผล

เมื่อเราประกาศ ส่วนต่างๆ ครบแล้วก็ทำการ export Store

```
export default new Vuex.Store<RootState>(store)
```

จะเห็นว่าเราใช้ Type RootState ตรงนี้อีกครั้ง แค่นี้เราก็ได้ Vuex ที่ support TypeScript แบบไม่ซับซ้อนมาก และ ไม่ต้องหาอะไรมาเพิ่มเติมด้วย

### Mixin

ผมแยกส่วนประกาศที่ต้องใช้บ่อยๆ ใน component คือ Vuex มาเก็บไว้เป็น mixin โดยสร้างไว้ที่

```
src/mixin/index.ts
```

และทำการประกาศดังนี้

```
import { Component, Vue } from 'vue-property-decorator'
import { mapActions, mapGetters } from 'vuex'

@Component({
  computed: mapGetters(['students', 'maxId', 'total', 'latest']),
  methods: { ...mapActions(['updateStudents', 'deleteStudents']) }
})
export default class Utils extends Vue {}
```

หน้าที่ของ mixin คือการนำเอาสิ่งที่ต้องใช้บ่อยๆ เช่นค่า data object, methods และ computed เป็นต้น มารวมไว้จะได้ไม่ต้องประกาศซ้ำๆ ตาม components ต่างๆ

ตัวอย่างผมสร้างไว้ชื่อ Utils แล้วทำการ mapActions และ mapGetters จาก Vuex ไว้ โดย เอา

- mapGetters ไปแปะใน computed จะเห็นชื่อของ getters ที่สร้างไว้
- mapActions ไปแปะใน methods จะเห็นชื่อ actions ที่สร้างไว้

การเขียน Vue.js แบบ TypeScript ที่ผมเลือกตอนสร้างจะเป็นแบบ class-style ซึ่งล่าสุด Vue Cli จะเลือก vue-property-decorator มาให้เลย

### Components

เมื่อเราได้ store เสร็จแล้ว มี mixin เรียบร้อยแล้ว ก็สามารถเขียน components เพื่อแสดงผลได้แล้ว

#### src/views/Add.vue

```
<template>
  <div class="about">
    <h1>Add New Student</h1>
    <div><label>FirstName:</label><input type="text" v-model="student.firstname" /></div>
    <div><label>LastName:</label><input type="text" v-model="student.lastname" /></div>
    <div><label>Age:</label><input type="number" max="50" min="7" v-model="student.age" /></div>
    <div>
      <button @click="addNew()">Add</button>
    </div>
    <hr />
    <h2>Total</h2>
    <div>{{ total }}</div>
    <div v-if="latest">
      <h2>Last Record:</h2>
      <table>
        <thead>
          <th>ID</th>
          <th>FullName</th>
          <th>Age</th>
        </thead>
        <tr>
          <td>{{ latest.id }}</td>
          <td>{{ latest.fullname }}</td>
          <td>{{ latest.age }}</td>
        </tr>
      </table>
    </div>
  </div>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
import Utils from '@/mixin'
import { Student } from '../type'

@Component({
  mixins: [Utils]
})
export default class Add extends Vue {
  maxId!: number
  updateStudents!: (student: Student) => void
  student = new Student()

  addNew() {
    const newId: number = this.maxId + 1
    this.student.id = newId
    this.updateStudents(this.student)
    this.student = new Student()
  }
}
</script>
```

- ใน template ผมสร้าง input มารับค่าต่างๆ คือ firstname, lastname และ age และ ปุ่มสำหรับ add ข้อมูล
- ใน script ผมเขียนแบบ class style โดยส่วนบนจะ import mixin และ type Student มาด้วย

@component เป็น decoration ที่สามารถจัดการพวก components ที่จะเอาเข้ามาใข้ จัดการ mixin จัดการ พวก mapGetters mapActions เป็นต้น ซึ่งจะต่างกับการเขียนแบบ javascript ธรรมดา

ตัวอย่างจะเห็นผมเรียกใช้ mixin ตรงส่วนนี้

```
@Component({ 
  mixins: [Utils]
})
```

เมื่อประกาศตรงนี้แล้วเราก็จะสามารถเรียกใช้ ค่าที่เรากำหนดใน mixin ได้เลยโดยเฉพาะใน template เรียกใช้ได้ทันที แต่ถ้าจะเรียกในส่วนของ Class จะต้องประกาศเพิ่มเติม ตามตัวอย่าง

```
export default class Add extends Vue {
  maxId!: number
  updateStudents!: (student: Student) => void
  student = new Student()

  addNew() {
    const newId: number = this.maxId + 1
    this.student.id = newId
    this.updateStudents(this.student)
    this.student = new Student()
  }
}
```

การประกาศ data object แบบ javascript จะเป็นแบบ

```
data: function () {
    return {
      message: 'hello',
      foo: 'abc'
    }
 }
```

แต่ถ้าใช้ TypeScript class style เราสามารถประกาศตัวแปร ด้านบนได้เลย

```
student = new Student()
```

แต่มีข้อแม้ว่า ต้องประกาศพร้อมค่าเริ่มต้นด้วย จากตัวอย่าง students จะกำหนดค่า ด้วยการสร้าง object ว่างๆ จาก new Student() ซึ่งตอนนี้เราสามารถที่จะ v-model input ใน template มายัง object student ได้แล้ว

```
<input type="text" v-model="student.firstname" />
<input type="text" v-model="student.lastname" />
<input type="number" max="50" min="7" v-model="student.age" />
```

เมื่อเราพิมพ์ค่าในช่องต่างๆ object student ก็จะถูก update ค่าต่างๆ ทันที

ส่วนค่า

```
maxId!: number
updateStudents!: (student: Student) => void
```

เป็นในส่วนของ Vuex ที่จะนำมาใช้ในส่วนของ methods ใน class ต้องประกาศ type ให้รู้จักก่อน ซึ่งลอกตามที่ประกาศไว้ใน store ได้เลย แต่ต้องใส่ ! ไว้ข้างหลังชื่อด้วย ถ้าเป็น function ต้องบอกว่า return เป็น type อะไรด้วยโดยใช้ => type

ย้ำอีกทีว่าถ้าใช้ใน template สามารถเรียกใช้ตามที่ประกาศใน mixin ได้เลยไม่ต้องมาประกาศ type ใน class

ทีนี้การเขียนแบบ class style พวก methods และ life-cycles ต่างๆ จะเขียนในระดับเดียวกัน คือจะเป็น method ของ class เช่น

```
export default class Add extends Vue {
  get nickname() {
    // computed
    return this.nickname
  }

  created(){
     // created life-cycle
  }
  login() {
    // method login
  }

}
```

สามารถอ่านเพิ่มเติมได้ที่

[kaorun343/vue-property-decorator](https://github.com/kaorun343/vue-property-decorator)

จากตัวอย่างผมมี method สำหรับ เพิ่มชื่อคือ

```
addNew() {
    const newId: number = this.maxId + 1
    this.student.id = newId
    this.updateStudents(this.student)
    this.student = new Student()
  }
```

ซึ่งผมจะเอาค่า maxId จาก store getter มาแล้วบวกเพิ่มเข้าไปอีกหนึ่ง แล้วทำการ กำหนดให้กับ object หลังจากนั้นก็ทำการ update state เมื่อเสร็จแล้วก็ให้ clear object เพื่อรอรับค่าต่อไป ตรงนี้ถ้าไม่ clear จะทำให้ค่าที่ได้ผิดเพี้ยนไปได้

เมื่อได้ method ก็สามารถที่จะ กำนหดให้กับ ปุ่มได้แล้ว

```
<button @click="addNew()">Add</button>
```

เมื่อกด add ข้อมูลด้านล่างจะแสดงจำนวนของ record ทั้งหมด และ record ล่าสุด

```
<div v-if="latest">
      <h2>Last Record:</h2>
      <table>
        <thead>
          <th>ID</th>
          <th>FullName</th>
          <th>Age</th>
        </thead>
        <tr>
          <td>{{ latest.id }}</td>
          <td>{{ latest.fullname }}</td>
          <td>{{ latest.age }}</td>
        </tr>
      </table>
    </div>
```

![](https://cdn-images-1.medium.com/max/587/1*LkEpUwnfUvLT7vmskcpa5g.png)<figcaption>กรอกข้อมูล</figcaption>

![](https://cdn-images-1.medium.com/max/1024/1*aLnb73t01RrMYxjWqI7w9Q.png)<figcaption>แสดงผล</figcaption>

ลอง add ไว้สักจำนวนหนึ่ง แล้วกดไปดูหน้า view

#### View

```
<template>
  <div>
    <h1>Students list</h1>
    <hr />
    <div v-if="students && latest">
      <h2>Total: {{ total }}</h2>
      <table>
        <thead>
          <th v-for="item in Object.keys(latest)" :key="item">
            {{ item.toUpperCase() }}
          </th>
          <th>ACTION</th>
        </thead>
        <tbody>
          <tr v-for="student in students" :key="student.id">
            <td v-for="(item, i) in Object.values(student)" :key="student.id + i + item">{{ item }}</td>
            <td><button @click="deleteStudents(student.id)">Delete</button></td>
          </tr>
        </tbody>
      </table>
    </div>
    <div v-else>
      <router-link :to="{ name: 'Add' }" tag="button">Add</router-link>
    </div>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
import Utils from '@/mixin'

@Component({
  mixins: [Utils]
})
export default class ViewList extends Vue {}
</script>
```

จากตัวอย่างจะเห็นว่าภายใน class ผมไม่ได้เขียนอะไรเพิ่มเลย ใช้ mixin เข้ามาผมก็สามารถเรียกในส่วนของ template ได้ทันที

![](https://cdn-images-1.medium.com/max/1024/1*FQSccWxiQLTf1MiYBHEfog.png)<figcaption>view</figcaption>

การใช้ Vuex ทำให้เราสามารถสลับไปมาระหว่าง component ได้โดยค่าจะไม่หายนั่นเองแต่ถ้า page โดน reload ค่าใน Vuex ก็จะหายไปเช่นกัน

### Vuex Modules

ถ้าเราจะแยก Vuex ออกเป็น modules ย่อยๆ เพื่อความเป็นระเบียบและ code ไม่รกรุงรักต้องทำยังไง ? ผมยกตัวอย่างง่ายๆ ให้ดูดังนี้นะครับ

ขั้นแรกต้องสร้าง Type ของ state ที่จำสร้างใหม่ขั้นมาก่อน โดยเพิ่มใน

src/type/index.ts

```
export class Teacher extends Student {

  subject: string

  constructor() {
    super()
    this.subject = ''
  }
}

export type Teachers = Teacher[]

export interface TeacherState {
  teachers: Teachers
}
```

สร้าง file module ย่อนใน src/store ได้เลย

src/store/teacher.ts

โดยมีโครงสร้างดังนี้

```
import { Module, ActionTree, MutationTree, GetterTree } from 'vuex'
import { RootState, TeacherState } from '@/type'

const state: TeacherState = {
 teachers: []
}
const mutations: MutationTree<TeacherState> = {
...
}
const actions: ActionTree<TeacherState, RootState> = {
...
}
const getters: GetterTree<TeacherState, RootState> = {
...
}
export const teachers: Module<TeacherState, RootState> = {
state,
getters,
actions,
mutations
}
```

ถ้าสงสัยว่าพวก

```
Module, ActionTree, MutationTree, GetterTree
```

คืออะไรก็ให้ไปดูใน

[https://github.com/vuejs/vuex/blob/v3.4.0/types/index.d.ts](https://github.com/vuejs/vuex/blob/v3.4.0/types/index.d.ts)

แล้วให้เพิ่มใน src/store/index.ts

```
modules: {
  teachers
}
```

ก็สามารถเพิ่ม module เข้าไปได้เรียบร้อย อาจจะ

หลักๆ ก็มีประมาณนี้เป็นการแนะนำ Vue.js TypeScript แบบสั้นๆ ถ้าสนใจสามารถศึกษาต่อยอดได้

ดู [Demo](https://mrchoke.github.io/vuex-typescript/)

ใครอ่านจนจบต้องยอมรับเลยว่าอ่านมาได้ยังไงจนจบ 😛