Chào mừng các bạn đến với Cafedev! Trong bài viết này, chúng ta sẽ cùng nhau khám phá về Vuejs với TypeScript và Composition API. Vuejs đã trở thành một trong những framework phát triển ứng dụng web phổ biến nhất, và việc kết hợp nó với TypeScript và Composition API sẽ mang lại những lợi ích to lớn cho quá trình phát triển của bạn. Hãy cùng Cafedev tìm hiểu cách sử dụng Vuejs một cách hiệu quả và linh hoạt thông qua TypeScript và Composition API ngay bây giờ!

Trang này giả định bạn đã đọc tổng quan về Sử dụng Vue với TypeScript.

1. Kiểu Thành Phần Props

1.1 Using <script setup>

<script setup lang="ts">
const props = defineProps({
  foo: { type: String, required: true },
  bar: Number
})

props.foo // string
props.bar // number | undefined
</script>

Điều này được gọi là “khai báo thời gian chạy”, vì đối số được truyền vào defineProps() sẽ được sử dụng như tùy chọn thời gian chạy props.
Tuy nhiên, thường thì việc định nghĩa props bằng các loại thuần túy thông qua đối số kiểu generic là một cách đơn giản hơn:

<script setup lang="ts">
const props = defineProps<{
  foo: string
  bar?: number
}>()
</script>

Điều này được gọi là “khai báo dựa trên kiểu”. Trình biên dịch sẽ cố gắng làm tốt nhất để suy luận các tùy chọn thời gian chạy tương đương dựa trên đối số kiểu. Trong trường hợp này, ví dụ thứ hai của chúng ta biên dịch thành các tùy chọn thời gian chạy chính xác giống như ví dụ đầu tiên.
Bạn có thể sử dụng cả hai cách khai báo dựa trên kiểu HOẶC thời gian chạy, nhưng bạn không thể sử dụng cả hai cùng một lúc.

Chúng ta cũng có thể di chuyển các loại props vào một giao diện riêng biệt:

<script setup lang="ts">
interface Props {
  foo: string
  bar?: number
}

const props = defineProps<Props>()
</script>

Điều này cũng hoạt động nếu Props được nhập từ một nguồn bên ngoài. Tính năng này yêu cầu TypeScript là một phụ thuộc cấp đồng nghiệp của Vue.

<script setup lang="ts">
import type { Props } from './foo'

const props = defineProps<Props>()
</script>

Hạn Chế Cú Pháp

Trong phiên bản 3.2 và dưới đây, tham số kiểu generic cho defineProps() bị giới hạn chỉ đến một loại chữ hoặc một tham chiếu đến một giao diện cục bộ.
Hạn chế này đã được giải quyết trong 3.3. Phiên bản mới nhất của Vue hỗ trợ việc tham chiếu các kiểu được nhập và một tập hợp giới hạn của các loại phức tạp trong vị trí tham số kiểu. Tuy nhiên, vì quá trình chuyển đổi kiểu thành thời gian chạy vẫn dựa trên AST, một số kiểu phức tạp đòi hỏi phân tích kiểu thực sự, ví dụ như các kiểu điều kiện, không được hỗ trợ. Bạn có thể sử dụng các kiểu điều kiện cho kiểu của một prop duy nhất, nhưng không phải toàn bộ đối tượng props.

1.2 Giá Trị Mặc Định của Props

Khi sử dụng khai báo dựa trên kiểu, chúng ta mất khả năng khai báo các giá trị mặc định cho các props. Điều này có thể được giải quyết bằng macro biên dịch withDefaults:

export interface Props {
  msg?: string
  labels?: string[]
}

const props = withDefaults(defineProps<Props>(), {
  msg: 'hello',
  labels: () => ['one', 'two']
})

Điều này sẽ được biên dịch thành các tùy chọn thời gian chạy default tương đương. Ngoài ra, trợ giúp withDefaults cung cấp kiểm tra kiểu cho các giá trị mặc định và đảm bảo rằng kiểu props được trả về đã được loại bỏ các cờ tùy chọn cho các thuộc tính có giá trị mặc định được khai báo.

1.3 Without <script setup>

Nếu không sử dụng <script setup>, cần sử dụng defineComponent() để cho phép suy luận kiểu dữ liệu của props. Kiểu của đối tượng props được truyền vào hàm setup() được suy luận từ tùy chọn props.

import { defineComponent } from 'vue'

export default defineComponent({
  props: {
    message: String
  },
  setup(props) {
    props.message // <-- type: string
  }
})

1.4 Các Kiểu Prop Phức Tạp

Với khai báo dựa trên kiểu, một prop có thể sử dụng một kiểu phức tạp giống như bất kỳ kiểu nào khác:

<script setup lang="ts">
interface Book {
  title: string
  author: string
  year: number
}

const props = defineProps<{
  book: Book
}>()
</script>

Đối với khai báo thời gian chạy, chúng ta có thể sử dụng kiểu tiện ích PropType:

import type { PropType } from 'vue'

const props = defineProps({
  book: Object as PropType<Book>
})

Điều này hoạt động tương tự nếu chúng ta đang chỉ định tùy chọn props trực tiếp:

import { defineComponent } from 'vue'
import type { PropType } from 'vue'

export default defineComponent({
  props: {
    book: Object as PropType<Book>
  }
})

Tùy chọn props thường được sử dụng nhiều hơn với API Tùy chọn, vì vậy bạn sẽ tìm thấy ví dụ chi tiết hơn trong hướng dẫn về TypeScript với Options API. Các kỹ thuật được hiển thị trong các ví dụ đó cũng áp dụng cho các khai báo thời gian chạy sử dụng defineProps().

2. Kiểu sự Phát Ra Của Thành Phần

<script setup lang="ts">
// runtime
const emit = defineEmits(['change', 'update'])

// options based
const emit = defineEmits({
  change: (id: number) => {
    // return `true` or `false` to indicate
    // validation pass / fail
  },
  update: (value: string) => {
    // return `true` or `false` to indicate
    // validation pass / fail
  }
})

// type-based
const emit = defineEmits<{
  (e: 'change', id: number): void
  (e: 'update', value: string): void
}>()

// 3.3+: alternative, more succinct syntax
const emit = defineEmits<{
  change: [id: number]
  update: [value: string]
}>()
</script>

Đối số kiểu có thể là một trong các loại sau đây:
1. Một kiểu hàm có thể gọi, nhưng được viết dưới dạng một loại chữ với Chữ Ký Gọi. Nó sẽ được sử dụng làm kiểu của hàm emit được trả về.

2. Một loại chữ nơi các khóa là tên sự kiện và các giá trị là các kiểu mảng / tuple đại diện cho các tham số được chấp nhận bổ sung cho sự kiện. Ví dụ trên đang sử dụng các tuple được đặt tên để mỗi đối số có thể có một tên rõ ràng.
Như chúng ta có thể thấy, khai báo kiểu mang lại cho chúng ta sự kiểm soát chi tiết hơn về ràng buộc kiểu của các sự kiện được phát ra.

import { defineComponent } from 'vue'

export default defineComponent({
  emits: ['change'],
  setup(props, { emit }) {
    emit('change') // <-- type check / auto-completion
  }
})

3. Kiểu của ref()

Refs suy luận kiểu từ giá trị ban đầu:

import { ref } from 'vue'

// inferred type: Ref<number>
const year = ref(2020)

// => TS Error: Type 'string' is not assignable to type 'number'.
year.value = '2020'

Đôi khi chúng ta có thể cần chỉ định các kiểu phức tạp cho giá trị bên trong của một ref. Chúng ta có thể làm điều đó bằng cách sử dụng kiểu Ref:

import { ref } from 'vue'
import type { Ref } from 'vue'

const year: Ref<string | number> = ref('2020')

year.value = 2020 // ok!

Hoặc, bằng cách chuyển đối số generic khi gọi ref() để ghi đè lên việc suy luận mặc định:

// resulting type: Ref<string | number>
const year = ref<string | number>('2020')

year.value = 2020 // ok!

Nếu bạn chỉ định một đối số kiểu chung nhưng bỏ qua giá trị khởi tạo, kiểu kết quả sẽ là một kiểu liên hợp bao gồm undefined:

// inferred type: Ref<number | undefined>
const n = ref<number>()

4. Đặt kiểu cho reactive()

reactive() cũng ngầm định suy luận kiểu từ đối số của nó:

import { reactive } from 'vue'

// inferred type: { title: string }
const book = reactive({ title: 'Vue 3 Guide' })

Để đặt kiểu một thuộc tính reactive một cách rõ ràng, chúng ta có thể sử dụng các giao diện:

import { reactive } from 'vue'

interface Book {
  title: string
  year?: number
}

const book: Book = reactive({ title: 'Vue 3 Guide' })

ghi chú Không khuyến khích sử dụng đối số kiểu chung của reactive() vì kiểu trả về, xử lý việc giải gói ref lồng nhau, khác với kiểu đối số kiểu chung.

5. Đặt kiểu cho computed()

computed() suy luận kiểu dựa trên giá trị trả về của getter:

import { ref, computed } from 'vue'

const count = ref(0)

// inferred type: ComputedRef<number>
const double = computed(() => count.value * 2)

// => TS Error: Property 'split' does not exist on type 'number'
const result = double.value.split('')

Bạn cũng có thể chỉ định một kiểu rõ ràng thông qua đối số kiểu chung:

const double = computed<number>(() => {
  // type error if this doesn't return a number
})

6. Đặt kiểu cho Xử lý Sự kiện

Khi xử lý các sự kiện DOM nguyên thuỷ, việc đặt kiểu đúng cho đối số chúng ta truyền vào bộ xử lý có thể hữu ích. Hãy xem xét ví dụ này:

<script setup lang="ts">
function handleChange(event) {
  // `event` implicitly has `any` type
  console.log(event.target.value)
}
</script>
<template>
  <input type="text" @change="handleChange" />
</template>

Nếu không có chú thích kiểu, đối số event sẽ ngầm định có kiểu any. Điều này cũng sẽ dẫn đến lỗi TS nếu "strict": true hoặc "noImplicitAny": true được sử dụng trong tsconfig.json. Do đó, được khuyến nghị là phải ghi chú kiểu của đối số trong các bộ xử lý sự kiện một cách rõ ràng. Ngoài ra, bạn có thể cần sử dụng khai báo kiểu khi truy cập các thuộc tính của event:

function handleChange(event: Event) {
  console.log((event.target as HTMLInputElement).value)
}

7. Đặt kiểu cho Provide / Inject

Provide và inject thường được thực hiện trong các thành phần riêng biệt. Để đặt kiểu cho các giá trị được inject, Vue cung cấp một giao diện InjectionKey, là một kiểu chung mở rộng từ Symbol. Nó có thể được sử dụng để đồng bộ kiểu của giá trị được inject giữa nhà cung cấp và người tiêu dùng:

import { provide, inject } from 'vue'
import type { InjectionKey } from 'vue'

const key = Symbol() as InjectionKey<string>

provide(key, 'foo') // providing non-string value will result in error

const foo = inject(key) // type of foo: string | undefined

Được khuyến khích đặt khóa injection trong một tệp riêng biệt để có thể được nhập vào trong nhiều thành phần.”

Khi sử dụng khóa injection dạng chuỗi, kiểu của giá trị được inject sẽ là unknown, và cần phải được khai báo một cách rõ ràng thông qua một đối số kiểu chung:

const foo = inject<string>('foo') // type: string | undefined

Lưu ý rằng giá trị được inject vẫn có thể là undefined, vì không có đảm bảo rằng một nhà cung cấp sẽ cung cấp giá trị này khi chạy.
Kiểu undefined có thể được loại bỏ bằng cách cung cấp một giá trị mặc định:

const foo = inject<string>('foo', 'bar') // type: string

Nếu bạn chắc chắn rằng giá trị luôn được cung cấp, bạn cũng có thể ép buộc giá trị:

const foo = inject('foo') as string

8. Đặt kiểu cho Template Refs

Refs của template nên được tạo với một đối số kiểu chung rõ ràng và một giá trị khởi tạo là null:

<script setup lang="ts">
import { ref, onMounted } from 'vue'

const el = ref<HTMLInputElement | null>(null)

onMounted(() => {
  el.value?.focus()
})
</script>
<template>
  <input ref="el" />
</template>

Để có được giao diện DOM đúng, bạn có thể kiểm tra các trang như MDN.
Lưu ý rằng đối với tính an toàn kiểu nghiêm ngặt, việc sử dụng chuỗi tùy chọn hoặc các kiểm tra kiểu khi truy cập el.value là cần thiết. Điều này là vì giá trị ref ban đầu là null cho đến khi thành phần được gắn kết, và nó cũng có thể được đặt thành null nếu phần tử được tham chiếu bị gỡ bỏ bởi v-if.

9. Đặt kiểu cho Refs của Template Component

Đôi khi bạn có thể cần ghi chú một ref của template cho một thành phần con để gọi phương thức công khai của nó. Ví dụ, chúng ta có một thành phần con MyModal với một phương thức mở modal:

<!-- MyModal.vue -->
<script setup lang="ts">
import { ref } from 'vue'

const isContentShown = ref(false)
const open = () => (isContentShown.value = true)

defineExpose({
  open
})
</script>

Để có được kiểu instance của MyModal, chúng ta cần trước tiên nhận kiểu của nó thông qua typeof, sau đó sử dụng tiện ích InstanceType tích hợp sẵn trong TypeScript để trích xuất kiểu instance của nó:


<!-- App.vue -->
<script setup lang="ts">
import MyModal from './MyModal.vue'

const modal = ref<InstanceType<typeof MyModal> | null>(null)

const openModal = () => {
  modal.value?.open()
}
</script>

Trong những trường hợp mà kiểu chính xác của thành phần không có sẵn hoặc không quan trọng, ComponentPublicInstance có thể được sử dụng thay vào đó. Điều này chỉ bao gồm các thuộc tính được chia sẻ bởi tất cả các thành phần, như $el:

import { ref } from 'vue'
import type { ComponentPublicInstance } from 'vue'

const child = ref<ComponentPublicInstance | null>(null)

Chúng ta đã cùng nhau khám phá về Vuejs với TypeScript và Composition API trong bài viết này trên Cafedev. Việc kết hợp giữa Vuejs, TypeScript và Composition API không chỉ giúp tăng cường hiệu suất phát triển mà còn đem lại sự linh hoạt và dễ dàng trong quá trình xây dựng ứng dụng web. Hy vọng rằng thông tin trong bài viết đã giúp bạn hiểu rõ hơn về cách sử dụng Vuejs trong môi trường TypeScript và Composition API. Đừng quên tiếp tục theo dõi Cafedev để cập nhật thêm nhiều thông tin hữu ích khác nhé!

Các nguồn kiến thức MIỄN PHÍ VÔ GIÁ từ cafedev tại đây

Nếu bạn thấy hay và hữu ích, bạn có thể tham gia các kênh sau của CafeDev để nhận được nhiều hơn nữa:

Chào thân ái và quyết thắng!

Đăng ký kênh youtube để ủng hộ Cafedev nha các bạn, Thanks you!