# 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 และตรรกะที่นำกลับมาใช้ซ้ำได้ ไวยากรณ์ `
{{ props.name }}
{{ props.role }}
```
ตอนนี้ 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
{{ label }}
```
> **กับดักด้าน 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