# Vue 3 với TypeScript năm 2026: Props, Emits và Composable an toàn kiểu > Làm chủ component Vue 3 an toàn kiểu với TypeScript: defineProps generic, defineEmits dạng tuple, composable có kiểu, defineModel và InjectionKey, kèm câu hỏi phỏng vấn. - Published: 2026-07-07 - Updated: 2026-07-07 - Author: SharpSkill - Tags: Vue 3, TypeScript, defineProps, Composables, Frontend - Reading time: 8 min --- Vue 3 kết hợp TypeScript vào năm 2026 mang lại cho các component khả năng kiểm tra kiểu tĩnh đầy đủ trên props, sự kiện và logic tái sử dụng. Cú pháp ` ``` Prop `role` giờ đây bị ràng buộc trong hai chuỗi ký tự literal, nên việc truyền `role="guest"` sẽ khiến quá trình build thất bại. Đây chính là lý do cốt lõi để chọn `defineProps` với TypeScript: [hướng dẫn TypeScript chính thức của Vue](https://vuejs.org/guide/typescript/composition-api.html) xem dạng generic là mặc định cho các dự án dùng ` ``` Vue 3.5 đã ổn định hóa **reactive props destructure**, hiện là mẫu gọn gàng hơn. Việc destructure kết quả của `defineProps` và gán giá trị mặc định trong cùng một câu lệnh vẫn giữ tính phản ứng đầy đủ — compiler viết lại mỗi lần truy cập thành `props.x` phía sau hậu trường. ```vue ``` > **Bẫy phản ứng với props được destructure** > > Props được destructure vẫn giữ tính phản ứng trong template và trong `computed`, nhưng việc truyền một giá trị đã destructure thẳng vào `watch` hoặc vào một composable sẽ đọc nó một lần và cắt đứt liên kết phản ứng. Hãy bọc nó trong một getter — `watch(() => color, ...)` — hoặc chuyển đổi bằng `toRef(props, 'color')` khi cần một ref ở phía sau. Đây là một cái bẫy phỏng vấn phổ biến: ứng viên tưởng rằng biến được destructure là một giá trị thuần túy, trong khi thực chất compiler đã đấu nối lại mỗi lần đọc về `props.color`. ## Emits an toàn kiểu với defineEmits Sự kiện xứng đáng được xử lý chặt chẽ như props. `defineEmits` dạng generic mô tả mỗi tên sự kiện và payload của nó dưới dạng một tuple, mang lại cho component cha khả năng tự hoàn thành và cho component con việc kiểm tra tại thời điểm biên dịch rằng đúng đối số đã được phát ra. ```vue ``` Dạng tuple này đã thay thế cú pháp chữ ký lời gọi cũ (`(e: 'search', q: string): void`) vì nó dễ đọc hơn và hỗ trợ nhiều sự kiện mà không cần overload. Khi một sự kiện không mang dữ liệu, tuple rỗng `[]` ghi rõ điều đó một cách tường minh. Kết hợp emits có kiểu với props có kiểu tạo ra những component mà toàn bộ giao diện công khai có thể kiểm chứng trước khi chạy — cùng một kỷ luật được đề cập trong [hướng dẫn Vue composition API](/blog/vue-nuxt/vue-3-composition-api-complete-guide). ## Khai báo kiểu cho composable dùng cho logic tái sử dụng Composable là các hàm thông thường, nên chúng tuân theo các quy tắc TypeScript bình thường — nhưng vài quy ước giúp chúng dễ dùng hơn. Hãy trả về kiểu `Ref` một cách tường minh khi việc suy luận không rõ ràng, và dùng generic khi một composable bao bọc dữ liệu tùy ý như một phản hồi API. ```ts // useFetch.ts import { ref, type Ref } from 'vue' interface UseFetchReturn { data: Ref error: Ref loading: Ref } // Generic flows through to the caller's typed data export function useFetch(url: string): UseFetchReturn { const data = ref(null) as Ref const error = ref(null) const loading = ref(true) fetch(url) .then((r) => r.json()) .then((json: T) => { data.value = json }) .catch((e: Error) => { error.value = e }) .finally(() => { loading.value = false }) return { data, error, loading } } ``` Tại nơi gọi, tham số generic khiến `data` có kiểu đầy đủ mà không cần chú thích thêm: ```ts // UserList.vue (script setup) interface User { id: number; name: string } // data is Ref — inferred from the generic const { data: users, loading } = useFetch('/api/users') ``` Việc chú thích tường minh đối tượng trả về (`UseFetchReturn`) đáng để bỏ ra vài dòng thêm: nó ghi lại hợp đồng, ngăn rò rỉ tình cờ các ref nội bộ, và cho người dùng một kiểu duy nhất để import. Về các mẫu composable sâu hơn như overload đối số và dọn dẹp có nhận biết vòng đời, hãy xem [hướng dẫn composable Vue nâng cao](/blog/vue-nuxt/advanced-vue-3-composables-reusable-patterns). ## Khai báo kiểu cho defineModel dùng cho two-way binding `defineModel`, ổn định kể từ Vue 3.4, gộp cặp prop `modelValue` cũ và emit `update:modelValue` thành một ref có thể ghi duy nhất. Tham số generic của nó khai báo kiểu cho cả hai chiều của ràng buộc cùng lúc. ```vue ``` Model có tên — `defineModel('title')` — ánh xạ tới `v-model:title` và nhận cùng cách khai báo kiểu. Điều này loại bỏ cả một lớp lỗi payload không khớp mà mẫu prop-và-emit thủ công từng che giấu. ## Khai báo kiểu cho template ref và instance của component Truy cập một nút DOM hoặc một component con thông qua một `ref` là nơi mã Vue không có kiểu thường rơi về `any` nhất. Cách khắc phục là tham số hóa `useTemplateRef` (Vue 3.5+) hoặc chính `ref` với kiểu phần tử, để việc truy cập thuộc tính được kiểm tra dựa trên giao diện DOM thực tế. ```vue ``` Đối với một tham chiếu tới một component con, `InstanceType` trích xuất kiểu công khai của component đó, phơi bày bất cứ gì component con đã khai báo qua `defineExpose`. Điều này giữ cho các lời gọi phương thức từ cha đến con được kiểm tra đầy đủ thay vì phải phỏng đoán. ```vue ``` ## Provide và inject an toàn kiểu với InjectionKey Việc tiêm phụ thuộc xuyên qua cây component sẽ mất thông tin kiểu trừ khi key mang theo nó. `InjectionKey` là một symbol có kiểu ràng buộc kiểu của một giá trị với key của nó, để `provide` và `inject` luôn đồng bộ mà không cần ép kiểu thủ công. ```ts // theme-key.ts import type { InjectionKey, Ref } from 'vue' export interface ThemeContext { mode: Ref<'light' | 'dark'> toggle: () => void } // The key permanently associates the ThemeContext type with this symbol export const ThemeKey: InjectionKey = Symbol('theme') ``` ```ts // provider (script setup) — value must match ThemeContext or it fails to compile provide(ThemeKey, { mode, toggle }) // consumer — theme is inferred as ThemeContext | undefined const theme = inject(ThemeKey) theme?.toggle() ``` Cung cấp một giá trị có hình dạng không khớp với `ThemeContext` là một lỗi biên dịch ngay tại nơi tiêm, bắt được sự lệch pha giữa các component ở xa nhau trước khi phát hành. ## Các câu hỏi phỏng vấn TypeScript Vue thường gặp Người phỏng vấn thăm dò xem ứng viên có hiểu ranh giới giữa kiểu tại thời điểm biên dịch và hành vi lúc chạy hay không. Vài câu hỏi thường lặp lại cùng câu trả lời sắc bén: - **Vì sao nên ưu tiên `defineProps()` hơn dạng đối tượng lúc chạy?** Props generic có thể diễn đạt kiểu union, chữ ký hàm và hình dạng lồng nhau mà khai báo lúc chạy không làm được, đồng thời loại bỏ việc trùng lặp định nghĩa kiểu/lúc chạy. - **Props được destructure có phản ứng không?** Có, trên Vue 3.5+, vì compiler viết lại truy cập thành `props.x`. Nhưng *giá trị* đã destructure được truyền vào `watch` hoặc vào một composable chỉ được đọc một lần — hãy dùng getter hoặc `toRef`. - **Làm sao khai báo kiểu cho một sự kiện phát ra không có payload?** Một tuple rỗng: `defineEmits<{ close: [] }>()`. - **`defineModel` thay thế cái gì?** Cặp prop `modelValue` và emit `update:modelValue`, hợp nhất thành một ref có thể ghi có kiểu. Luyện tập những điều này với một ngân hàng câu hỏi thực tế mài giũa phản xạ mà phỏng vấn tưởng thưởng — [mô-đun phỏng vấn composable Vue](/technologies/vue-nuxt/interview-questions/vue-composables) rèn luyện chính xác những mẫu này. Công cụ cũng quan trọng: chạy `vue-tsc` trong CI để lỗi kiểu chặn việc merge, và dựa vào [sổ tay TypeScript](https://www.typescriptlang.org/docs/handbook/2/generics.html) khi generic của composable bắt đầu tham gia. Trải nghiệm trình soạn thảo được cung cấp bởi [công cụ Volar chính thức của Vue](https://github.com/vuejs/language-tools), vốn đọc các macro này để hiển thị lỗi ngay tại chỗ. ## Kết luận - Dùng `defineProps()` dạng generic để diễn đạt kiểu union, field tùy chọn và prop callback mà khai báo lúc chạy không nắm bắt được. - Ưu tiên reactive props destructure với giá trị mặc định tại chỗ trên Vue 3.5+, và chỉ dùng đến `withDefaults` khi một đối tượng giá trị mặc định dùng chung rõ ràng hơn. - Khai báo kiểu cho sự kiện bằng dạng tuple của `defineEmits` để payload được kiểm tra tại thời điểm biên dịch ở cả con lẫn cha. - Chú thích tường minh kiểu trả về của composable và dùng generic để chuyển tiếp kiểu dữ liệu do người gọi cung cấp từ đầu đến cuối. - Áp dụng `defineModel()` cho two-way binding để gộp mẫu boilerplate prop-cộng-emit thành một ref có kiểu. - Chạy `vue-tsc` trong tích hợp liên tục để hồi quy kiểu làm thất bại quá trình build thay vì lọt tới sản phẩm. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/vi/blog/vue-nuxt/vue-3-typescript-type-safe-props-emits-composables