Vue + TypeScript
Vue 3 是用 TypeScript 編寫的,提供了出色的 TypeScript 支援。使用 TypeScript 可以獲得更好的 IDE 支援、程式碼提示和型別檢查。
建立專案
使用 create-vue 建立支援 TypeScript 的專案:
npm create vue@latest
# 選擇 "Add TypeScript?" -> Yes
或手動加入 TypeScript:
npm install -D typescript vue-tsc
單文件元件中使用 TypeScript
在 <script setup> 中加入 lang="ts":
<script setup lang="ts">
import { ref } from 'vue'
// 型別推斷
const count = ref(0) // Ref<number>
const message = ref('Hello') // Ref<string>
// 明確型別
const items = ref<string[]>([])
const user = ref<User | null>(null)
</script>
定義 Props
執行時宣告
<script setup lang="ts">
const props = defineProps({
title: {
type: String,
required: true
},
count: {
type: Number,
default: 0
},
items: {
type: Array as PropType<string[]>,
default: () => []
}
})
</script>
型別宣告(推薦)
<script setup lang="ts">
interface Props {
title: string
count?: number
items?: string[]
user?: {
name: string
age: number
}
}
const props = defineProps<Props>()
</script>
有預設值的 Props
<script setup lang="ts">
interface Props {
title: string
count?: number
items?: string[]
}
const props = withDefaults(defineProps<Props>(), {
count: 0,
items: () => []
})
</script>
定義 Emits
執行時宣告
<script setup lang="ts">
const emit = defineEmits(['update', 'delete'])
// 帶驗證
const emit = defineEmits({
update: (value: string) => true,
delete: (id: number) => id > 0
})
</script>
型別宣告(推薦)
<script setup lang="ts">
// 函式簽名語法
const emit = defineEmits<{
(e: 'update', value: string): void
(e: 'delete', id: number): void
}>()
// Vue 3.3+ 更簡潔的語法
const emit = defineEmits<{
update: [value: string]
delete: [id: number]
}>()
</script>
定義 Ref
<script setup lang="ts">
import { ref, reactive } from 'vue'
// 基本型別
const count = ref<number>(0)
const message = ref<string>('')
const isActive = ref<boolean>(false)
// 複雜型別
interface User {
id: number
name: string
email: string
}
const user = ref<User | null>(null)
const users = ref<User[]>([])
// reactive
const state = reactive<{
count: number
users: User[]
}>({
count: 0,
users: []
})
</script>
定義 Computed
<script setup lang="ts">
import { ref, computed } from 'vue'
const firstName = ref('John')
const lastName = ref('Doe')
// 型別會自動推斷
const fullName = computed(() => {
return `${firstName.value} ${lastName.value}`
})
// 明確指定型別
const fullName = computed<string>(() => {
return `${firstName.value} ${lastName.value}`
})
// 可寫 computed
const fullName = computed({
get(): string {
return `${firstName.value} ${lastName.value}`
},
set(value: string) {
const names = value.split(' ')
firstName.value = names[0]
lastName.value = names[1]
}
})
</script>
Template Refs
<script setup lang="ts">
import { ref, onMounted } from 'vue'
// DOM 元素
const inputRef = ref<HTMLInputElement | null>(null)
onMounted(() => {
inputRef.value?.focus()
})
// 元件實例
import MyComponent from './MyComponent.vue'
const compRef = ref<InstanceType<typeof MyComponent> | null>(null)
</script>
<template>
<input ref="inputRef" type="text">
<MyComponent ref="compRef" />
</template>
事件處理
<script setup lang="ts">
function handleClick(event: MouseEvent) {
console.log(event.clientX, event.clientY)
}
function handleInput(event: Event) {
const target = event.target as HTMLInputElement
console.log(target.value)
}
function handleSubmit(event: Event) {
event.preventDefault()
// ...
}
</script>
<template>
<button @click="handleClick">Click</button>
<input @input="handleInput">
<form @submit="handleSubmit">...</form>
</template>
Provide / Inject
<!-- 父元件 -->
<script setup lang="ts">
import { provide, ref } from 'vue'
import type { InjectionKey, Ref } from 'vue'
interface User {
name: string
age: number
}
// 定義 key 並指定型別
export const userKey: InjectionKey<Ref<User>> = Symbol('user')
const user = ref<User>({ name: 'John', age: 30 })
provide(userKey, user)
</script>
<!-- 子元件 -->
<script setup lang="ts">
import { inject } from 'vue'
import { userKey } from './parent.vue'
// 型別會正確推斷
const user = inject(userKey)
// 帶預設值
const user = inject(userKey, ref({ name: 'Default', age: 0 }))
</script>
defineExpose
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
// 暴露的內容會有正確的型別
defineExpose({
count,
increment
})
</script>
defineModel
<script setup lang="ts">
// 基本
const model = defineModel<string>()
// 必填
const model = defineModel<string>({ required: true })
// 有預設值
const model = defineModel<number>({ default: 0 })
// 具名
const title = defineModel<string>('title')
</script>
Composables 的型別
// composables/useCounter.ts
import { ref, computed } from 'vue'
import type { Ref, ComputedRef } from 'vue'
interface UseCounterReturn {
count: Ref<number>
doubled: ComputedRef<number>
increment: () => void
decrement: () => void
}
export function useCounter(initialValue = 0): UseCounterReturn {
const count = ref(initialValue)
const doubled = computed(() => count.value * 2)
function increment() {
count.value++
}
function decrement() {
count.value--
}
return {
count,
doubled,
increment,
decrement
}
}
// composables/useFetch.ts
import { ref, watchEffect, toValue } from 'vue'
import type { Ref, MaybeRefOrGetter } from 'vue'
interface UseFetchReturn<T> {
data: Ref<T | null>
error: Ref<Error | null>
isLoading: Ref<boolean>
}
export function useFetch<T>(url: MaybeRefOrGetter<string>): UseFetchReturn<T> {
const data = ref<T | null>(null) as Ref<T | null>
const error = ref<Error | null>(null)
const isLoading = ref(false)
watchEffect(async () => {
isLoading.value = true
error.value = null
try {
const response = await fetch(toValue(url))
data.value = await response.json()
} catch (e) {
error.value = e as Error
} finally {
isLoading.value = false
}
})
return { data, error, isLoading }
}
Pinia Store 的型別
// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
interface User {
id: number
name: string
email: string
}
export const useUserStore = defineStore('user', () => {
const user = ref<User | null>(null)
const token = ref<string | null>(null)
const isLoggedIn = computed(() => !!token.value)
async function login(email: string, password: string): Promise<void> {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password })
})
const data = await response.json()
user.value = data.user
token.value = data.token
}
function logout(): void {
user.value = null
token.value = null
}
return {
user,
token,
isLoggedIn,
login,
logout
}
})
Vue Router 的型別
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'
declare module 'vue-router' {
interface RouteMeta {
requiresAuth?: boolean
title?: string
roles?: string[]
}
}
const routes: RouteRecordRaw[] = [
{
path: '/',
name: 'home',
component: () => import('../views/Home.vue'),
meta: {
title: '首頁'
}
},
{
path: '/admin',
name: 'admin',
component: () => import('../views/Admin.vue'),
meta: {
requiresAuth: true,
roles: ['admin']
}
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
在元件中使用:
<script setup lang="ts">
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
// route.meta 會有正確的型別
console.log(route.meta.requiresAuth) // boolean | undefined
console.log(route.meta.roles) // string[] | undefined
</script>
全域屬性型別
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
// 添加全域屬性
app.config.globalProperties.$http = axios
app.config.globalProperties.$filters = {
currency(value: number): string {
return `$${value.toFixed(2)}`
}
}
// 型別擴展
declare module 'vue' {
interface ComponentCustomProperties {
$http: typeof axios
$filters: {
currency(value: number): string
}
}
}
tsconfig.json 設定
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}
常用型別
import type {
// 響應式
Ref,
ComputedRef,
ShallowRef,
ToRef,
ToRefs,
UnwrapRef,
// Props
PropType,
// 元件
Component,
DefineComponent,
// 其他
InjectionKey,
MaybeRef,
MaybeRefOrGetter,
// 事件
VNode
} from 'vue'
最佳實踐
- 優先使用型別宣告:對於 props 和 emits,使用泛型語法更簡潔
- 善用型別推斷:Vue 的型別推斷很強,不需要處處標註型別
- 定義介面:將複雜型別提取成介面
- 使用 strict 模式:在 tsconfig 中啟用嚴格模式
- 善用 IDE:VS Code + Volar 提供最佳的開發體驗