# Vue 3 กับ TypeScript ในปี 2026: Props, Emits และ Composable ที่ปลอดภัยด้านชนิดข้อมูล > เชี่ยวชาญคอมโพเนนต์ Vue 3 ที่ปลอดภัยด้านชนิดข้อมูลด้วย TypeScript: defineProps แบบ generic, defineEmits แบบ tuple, composable ที่มีชนิด, defineModel และ InjectionKey พร้อมคำถามสัมภาษณ์ - Published: 2026-07-07 - Updated: 2026-07-07 - Author: SharpSkill - Tags: Vue 3, TypeScript, defineProps, Composables, Frontend - Reading time: 8 min --- Vue 3 ที่ทำงานร่วมกับ TypeScript ในปี 2026 มอบการตรวจสอบชนิดข้อมูลแบบสแตติกอย่างสมบูรณ์ให้กับคอมโพเนนต์ทั้งในส่วนของ props, event และตรรกะที่นำกลับมาใช้ซ้ำได้ ไวยากรณ์ ` ``` ตอนนี้ prop `role` ถูกจำกัดให้เป็นสตริง literal เพียงสองค่า ดังนั้นการส่ง `role="guest"` จะทำให้กระบวนการ build ล้มเหลว นี่คือเหตุผลหลักที่ควรเลือกใช้ `defineProps` ร่วมกับ TypeScript โดย [คู่มือ TypeScript อย่างเป็นทางการของ Vue](https://vuejs.org/guide/typescript/composition-api.html) ถือว่ารูปแบบ generic เป็นค่าเริ่มต้นสำหรับโปรเจกต์ที่ใช้ ` ``` Vue 3.5 ได้ทำให้ **reactive props destructure** มีเสถียรภาพ ซึ่งปัจจุบันเป็นรูปแบบที่กระชับกว่า การ destructure ผลลัพธ์ของ `defineProps` และกำหนดค่าเริ่มต้นในคำสั่งเดียวกันยังคงความ reactive อย่างสมบูรณ์ เพราะ compiler จะเขียนการเข้าถึงแต่ละครั้งกลับไปเป็น `props.x` อยู่เบื้องหลัง ```vue ``` > **กับดักด้าน reactivity ของ props ที่ถูก destructure** > > props ที่ถูก destructure ยังคงความ reactive ทั้งในเทมเพลตและใน `computed` แต่การส่งค่าที่ถูก destructure โดยตรงเข้าไปใน `watch` หรือใน composable จะอ่านค่านั้นเพียงครั้งเดียวและตัดการเชื่อมโยงแบบ reactive ทิ้งไป ให้ห่อไว้ใน getter — `watch(() => color, ...)` — หรือแปลงด้วย `toRef(props, 'color')` เมื่อจำเป็นต้องใช้ ref ในขั้นตอนถัดไป นี่คือกับดักการสัมภาษณ์ที่พบได้บ่อย: ผู้สมัครมักเข้าใจว่าตัวแปรที่ถูก destructure เป็นค่าธรรมดา ทั้งที่จริงแล้ว compiler ได้เดินสายการอ่านทุกครั้งกลับไปยัง `props.color` แล้ว ## emits ที่ปลอดภัยด้านชนิดข้อมูลด้วย defineEmits event สมควรได้รับความเข้มงวดเช่นเดียวกับ props รูปแบบ generic ของ `defineEmits` อธิบายชื่อ event แต่ละตัวและ payload ของมันในรูปของ tuple ทำให้คอมโพเนนต์แม่ได้รับการเติมข้อความอัตโนมัติ และคอมโพเนนต์ลูกได้รับการตรวจสอบตอนคอมไพล์ว่าได้ส่งอาร์กิวเมนต์ที่ถูกต้องออกไป ```vue ``` รูปแบบ tuple นี้เข้ามาแทนที่ไวยากรณ์ลายเซ็นการเรียกแบบเดิม (`(e: 'search', q: string): void`) เพราะอ่านได้ง่ายกว่าและรองรับหลาย event โดยไม่ต้องใช้ overload เมื่อ event ไม่มีข้อมูลติดไปด้วย tuple เปล่า `[]` จะบันทึกเรื่องนี้ไว้อย่างชัดเจน การจับคู่ emits ที่มีชนิดข้อมูลเข้ากับ props ที่มีชนิดข้อมูลจะสร้างคอมโพเนนต์ที่อินเทอร์เฟซสาธารณะทั้งหมดสามารถตรวจสอบได้ก่อนรันไทม์ ซึ่งเป็นวินัยเดียวกับที่กล่าวถึงใน [คู่มือ Vue composition API](/blog/vue-nuxt/vue-3-composition-api-complete-guide) ## การกำหนดชนิดให้ composable สำหรับตรรกะที่นำกลับมาใช้ซ้ำ composable เป็นเพียงฟังก์ชันธรรมดา จึงเป็นไปตามกฎทั่วไปของ TypeScript แต่ธรรมเนียมปฏิบัติบางอย่างช่วยให้ใช้งานได้สะดวก ควรคืนค่าชนิด `Ref` อย่างชัดเจนเมื่อการอนุมานไม่ชัดเจน และใช้ generic เมื่อ composable ห่อหุ้มข้อมูลใดก็ได้ เช่น การตอบกลับจาก 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 } } ``` ณ จุดที่เรียกใช้ พารามิเตอร์ generic ทำให้ `data` มีชนิดข้อมูลครบถ้วนโดยไม่ต้องใส่คำอธิบายประกอบเพิ่มเติม: ```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') ``` การใส่คำอธิบายประกอบให้อ็อบเจกต์ที่คืนค่าอย่างชัดเจน (`UseFetchReturn`) คุ้มค่ากับโค้ดที่เพิ่มขึ้นไม่กี่บรรทัด เพราะมันบันทึกสัญญาไว้ ป้องกันการรั่วไหลของ ref ภายในโดยไม่ตั้งใจ และมอบชนิดข้อมูลชนิดเดียวให้ผู้ใช้นำเข้าไปใช้ สำหรับรูปแบบ composable ที่ลึกกว่านี้ เช่น การ overload อาร์กิวเมนต์และการล้างข้อมูลที่รับรู้วงจรชีวิต โปรดดู [คู่มือ composable ของ Vue ขั้นสูง](/blog/vue-nuxt/advanced-vue-3-composables-reusable-patterns) ## การกำหนดชนิดให้ defineModel สำหรับ two-way binding `defineModel` ซึ่งมีเสถียรภาพตั้งแต่ Vue 3.4 ยุบคู่ของ prop `modelValue` เดิมและ emit `update:modelValue` ให้เหลือเป็น ref ที่เขียนได้เพียงตัวเดียว พารามิเตอร์ generic ของมันกำหนดชนิดให้ทั้งสองทิศทางของ binding พร้อมกันในคราวเดียว ```vue ``` model ที่มีชื่อ — `defineModel('title')` — จะจับคู่กับ `v-model:title` และได้รับการกำหนดชนิดแบบเดียวกัน สิ่งนี้ขจัดบั๊กประเภท payload ที่ไม่ตรงกันทั้งกลุ่ม ซึ่งรูปแบบ prop-กับ-emit แบบทำมือเคยซ่อนเอาไว้ ## การกำหนดชนิดให้ template ref และอินสแตนซ์ของคอมโพเนนต์ การเข้าถึงโหนด DOM หรือคอมโพเนนต์ลูกผ่าน `ref` คือจุดที่โค้ด Vue ที่ไม่มีชนิดข้อมูลมักตกกลับไปเป็น `any` มากที่สุด วิธีแก้คือการใส่พารามิเตอร์ชนิดขององค์ประกอบให้กับ `useTemplateRef` (Vue 3.5+) หรือให้กับ `ref` เอง เพื่อให้การเข้าถึงพร็อพเพอร์ตีถูกตรวจสอบกับอินเทอร์เฟซ DOM ที่แท้จริง ```vue ``` สำหรับการอ้างอิงไปยังคอมโพเนนต์ลูก `InstanceType` จะสกัดชนิดสาธารณะของคอมโพเนนต์นั้นออกมา เปิดเผยทุกสิ่งที่ลูกประกาศไว้ผ่าน `defineExpose` สิ่งนี้ทำให้การเรียกเมธอดจากแม่ไปยังลูกถูกตรวจสอบอย่างครบถ้วนแทนที่จะต้องเดา ```vue ``` ## provide และ inject ที่ปลอดภัยด้านชนิดข้อมูลด้วย InjectionKey การฉีดพึ่งพา (dependency injection) ข้ามต้นไม้ของคอมโพเนนต์จะสูญเสียข้อมูลชนิด เว้นแต่ว่า key จะนำมันติดไปด้วย `InjectionKey` คือ symbol ที่มีชนิดข้อมูลซึ่งผูกชนิดของค่าเข้ากับ key ของมัน เพื่อให้ `provide` และ `inject` สอดคล้องกันโดยไม่ต้อง cast ด้วยมือ ```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() ``` การจัดหาค่าที่โครงสร้างไม่ตรงกับ `ThemeContext` จะเป็นข้อผิดพลาดตอนคอมไพล์ ณ จุดที่ฉีด ช่วยดักจับความคลาดเคลื่อนระหว่างคอมโพเนนต์ที่อยู่ห่างไกลกันก่อนที่จะปล่อยขึ้นใช้งานจริง ## คำถามสัมภาษณ์ TypeScript ของ Vue ที่พบบ่อย ผู้สัมภาษณ์มักตรวจสอบว่าผู้สมัครเข้าใจเส้นแบ่งระหว่างชนิดข้อมูลตอนคอมไพล์กับพฤติกรรมตอนรันไทม์หรือไม่ ต่อไปนี้คือคำถามที่พบซ้ำ ๆ พร้อมคำตอบที่คมชัด: - **เหตุใดจึงควรเลือก `defineProps()` มากกว่ารูปแบบอ็อบเจกต์แบบรันไทม์?** props แบบ generic สามารถแสดง union type, ลายเซ็นฟังก์ชัน และโครงสร้างแบบซ้อนที่การประกาศแบบรันไทม์ทำไม่ได้ อีกทั้งยังขจัดการนิยามชนิด/รันไทม์ที่ซ้ำซ้อน - **props ที่ถูก destructure ยังคง reactive หรือไม่?** ใช่ ใน Vue 3.5+ เพราะ compiler เขียนการเข้าถึงกลับไปเป็น `props.x` แต่ *ค่า* ที่ถูก destructure แล้วส่งเข้าไปใน `watch` หรือใน composable จะถูกอ่านเพียงครั้งเดียว ให้ใช้ getter หรือ `toRef` - **จะกำหนดชนิดให้ event ที่ส่งออกโดยไม่มี payload อย่างไร?** ใช้ tuple เปล่า: `defineEmits<{ close: [] }>()` - **`defineModel` เข้ามาแทนที่อะไร?** คู่ของ prop `modelValue` และ emit `update:modelValue` ที่ถูกรวมเป็น ref ที่เขียนได้และมีชนิดข้อมูลเพียงตัวเดียว การฝึกฝนสิ่งเหล่านี้กับคลังคำถามจริงจะช่วยลับปฏิกิริยาตอบสนองที่การสัมภาษณ์ให้ค่า — [โมดูลสัมภาษณ์ composable ของ Vue](/technologies/vue-nuxt/interview-questions/vue-composables) ฝึกฝนรูปแบบเหล่านี้อย่างตรงจุด เครื่องมือก็สำคัญเช่นกัน: รัน `vue-tsc` ใน CI เพื่อให้ข้อผิดพลาดด้านชนิดข้อมูลปิดกั้นการ merge และพึ่งพา [คู่มือ TypeScript](https://www.typescriptlang.org/docs/handbook/2/generics.html) เมื่อ generic ของ composable เริ่มเข้ามาเกี่ยวข้อง ประสบการณ์การใช้งานในเอดิเตอร์ขับเคลื่อนโดย [เครื่องมือ Volar อย่างเป็นทางการของ Vue](https://github.com/vuejs/language-tools) ซึ่งอ่าน macro เหล่านี้เพื่อแสดงข้อผิดพลาดแบบ inline ## บทสรุป - ใช้ `defineProps()` แบบ generic เพื่อแสดง union type, field ที่เป็นทางเลือก และ prop แบบ callback ที่การประกาศแบบรันไทม์ไม่สามารถจับต้องได้ - ให้ความสำคัญกับ reactive props destructure ที่มีค่าเริ่มต้นแบบ inline ใน Vue 3.5+ และหันไปใช้ `withDefaults` เฉพาะเมื่ออ็อบเจกต์ค่าเริ่มต้นที่ใช้ร่วมกันชัดเจนกว่า - กำหนดชนิดให้ event ด้วยรูปแบบ tuple ของ `defineEmits` เพื่อให้ payload ถูกตรวจสอบตอนคอมไพล์ทั้งในลูกและในแม่ - ใส่คำอธิบายประกอบชนิดที่คืนค่าของ composable อย่างชัดเจน และใช้ generic เพื่อส่งต่อชนิดข้อมูลที่ผู้เรียกจัดหาให้ตั้งแต่ต้นจนจบ - นำ `defineModel()` มาใช้กับ two-way binding เพื่อยุบโค้ด boilerplate แบบ prop-บวก-emit ให้เหลือเป็น ref ที่มีชนิดข้อมูลตัวเดียว - รัน `vue-tsc` ในระบบการรวมโค้ดแบบต่อเนื่อง เพื่อให้การถดถอยด้านชนิดข้อมูลทำให้การ build ล้มเหลว แทนที่จะหลุดไปถึงการใช้งานจริง --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/th/blog/vue-nuxt/vue-3-typescript-type-safe-props-emits-composables