第1章:TypeScript 核心
TypeScript 是 Vue 的强力外挂,掌握 TS 的 20% 核心特性就能覆盖 80% 的开发场景。本章将深入讲解类型系统、泛型、条件类型等高级特性,并结合 Vue 实际应用场景。
📋 本章内容
🎯 学习目标
- 掌握 TypeScript 核心类型系统
- 理解泛型、条件类型、映射类型等高级特性
- 熟练在 Vue 组件中使用 TypeScript
- 学会设计类型安全的 API
🔧 基础类型系统
基本类型
TypeScript 提供了丰富的基本类型:
// 基本类型
let name: string = 'Vue'
let age: number = 3
let isActive: boolean = true
let items: string[] = ['a', 'b', 'c']
let user: { name: string; age: number } = { name: 'John', age: 25 }
// 联合类型
let id: string | number = '123'
id = 456 // 也合法
// 字面量类型
let status: 'loading' | 'success' | 'error' = 'loading'
类型断言
类型断言告诉编译器"我知道这个值的类型":
// 方式1:as 语法
const element = document.getElementById('app') as HTMLDivElement
// 方式2:尖括号语法(在 JSX 中不推荐)
const element2 = <HTMLDivElement>document.getElementById('app')
// 非空断言
const user = getUser()! // 告诉 TS 这个值不会是 null/undefined
as const 断言
as const
将值标记为不可变的字面量类型:
// 普通数组
const colors = ['red', 'green', 'blue'] // string[]
// 使用 as const
const colors = ['red', 'green', 'blue'] as const // readonly ['red', 'green', 'blue']
// 对象使用 as const
const config = {
api: 'https://api.example.com',
timeout: 5000
} as const
// 类型:{ readonly api: 'https://api.example.com'; readonly timeout: 5000 }
satisfies 操作符
satisfies
确保值符合类型,同时保持更精确的类型推断:
// 使用 satisfies 保持精确类型
const theme = {
primary: '#42b883',
secondary: '#35495e'
} satisfies Record<string, string>
// theme.primary 的类型是 '#42b883' 而不是 string
🚀 高级类型特性
泛型基础
泛型允许我们创建可重用的组件,这些组件可以处理多种类型:
// 基础泛型函数
function identity<T>(arg: T): T {
return arg
}
const result1 = identity<string>('hello') // 显式指定类型
const result2 = identity('hello') // 类型推断
// 泛型接口
interface ApiResponse<T> {
data: T
status: number
message: string
}
// 泛型类
class Container<T> {
private items: T[] = []
add(item: T): void {
this.items.push(item)
}
get(index: number): T | undefined {
return this.items[index]
}
}
泛型约束
使用 extends
关键字约束泛型类型:
// 约束泛型必须有 length 属性
function logLength<T extends { length: number }>(arg: T): T {
console.log(arg.length)
return arg
}
logLength('hello') // ✅ string 有 length
logLength([1, 2, 3]) // ✅ array 有 length
logLength(123) // ❌ number 没有 length
// 约束泛型必须是对象
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]
}
const user = { name: 'John', age: 25 }
const name = getProperty(user, 'name') // string
const age = getProperty(user, 'age') // number
条件类型
条件类型允许根据条件选择类型:
// 基础条件类型
type IsString<T> = T extends string ? true : false
type Test1 = IsString<string> // true
type Test2 = IsString<number> // false
// 分布式条件类型
type ToArray<T> = T extends any ? T[] : never
type Test3 = ToArray<string | number> // string[] | number[]
// 实用条件类型
type NonNullable<T> = T extends null | undefined ? never : T
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never
映射类型
映射类型允许基于旧类型创建新类型:
// 基础映射类型
type Partial<T> = {
[P in keyof T]?: T[P]
}
type Required<T> = {
[P in keyof T]-?: T[P]
}
type Readonly<T> = {
readonly [P in keyof T]: T[P]
}
// 自定义映射类型
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
interface User {
id: number
name: string
email: string
password: string
}
type CreateUser = Optional<User, 'id'> // 创建用户时 id 可选
type UpdateUser = Optional<User, 'id' | 'password'> // 更新时 id 和 password 可选
模板字面量类型
TypeScript 4.1+ 支持模板字面量类型:
type EventName<T extends string> = `on${Capitalize<T>}`
type ClickEvent = EventName<'click'> // 'onClick'
type ChangeEvent = EventName<'change'> // 'onChange'
// 结合条件类型
type Getter<T extends string> = T extends `get${infer U}` ? U : never
type Name = Getter<'getName'> // 'Name'
type Age = Getter<'getAge'> // 'Age'
🎨 Vue 中的 TypeScript
组件 Props 类型
在 Vue 3 中,我们可以为组件 props 提供强类型:
// 方式1:使用泛型
interface Props {
title: string
count?: number
items: string[]
}
const props = defineProps<Props>()
// 方式2:使用运行时声明(推荐,支持默认值)
const props = withDefaults(defineProps<Props>(), {
count: 0,
items: () => []
})
// 方式3:使用 defineProps 的运行时声明
const props = defineProps({
title: {
type: String,
required: true
},
count: {
type: Number,
default: 0
},
items: {
type: Array as PropType<string[]>,
default: () => []
}
})
组件 Emits 类型
为组件事件提供类型安全:
// 方式1:使用泛型
interface Emits {
(e: 'update:modelValue', value: string): void
(e: 'change', value: string, oldValue: string): void
(e: 'submit', data: FormData): void
}
const emit = defineEmits<Emits>()
// 方式2:使用运行时声明
const emit = defineEmits<{
'update:modelValue': [value: string]
'change': [value: string, oldValue: string]
'submit': [data: FormData]
}>()
// 使用
emit('update:modelValue', 'new value')
emit('change', 'new', 'old')
组合式 API 类型
为组合式函数提供类型支持:
// 为 ref 提供类型
const count = ref<number>(0)
const user = ref<User | null>(null)
// 为 reactive 提供类型
interface State {
loading: boolean
data: any[]
error: string | null
}
const state = reactive<State>({
loading: false,
data: [],
error: null
})
// 为 computed 提供类型
const doubleCount = computed<number>(() => count.value * 2)
const filteredData = computed<State['data']>(() =>
state.data.filter(item => item.active)
)
自定义组合式函数
创建类型安全的组合式函数:
// useFetch 组合式函数
interface UseFetchOptions {
immediate?: boolean
onSuccess?: (data: any) => void
onError?: (error: Error) => void
}
interface UseFetchReturn<T> {
data: Ref<T | null>
loading: Ref<boolean>
error: Ref<Error | null>
execute: () => Promise<void>
}
function useFetch<T>(
url: string,
options: UseFetchOptions = {}
): UseFetchReturn<T> {
const data = ref<T | null>(null)
const loading = ref(false)
const error = ref<Error | null>(null)
const execute = async () => {
loading.value = true
error.value = null
try {
const response = await fetch(url)
const result = await response.json()
data.value = result
options.onSuccess?.(result)
} catch (err) {
error.value = err as Error
options.onError?.(err as Error)
} finally {
loading.value = false
}
}
if (options.immediate) {
execute()
}
return { data, loading, error, execute }
}
// 使用
const { data, loading, error } = useFetch<User[]>('/api/users', {
immediate: true,
onSuccess: (users) => console.log('Users loaded:', users)
})
🎯 类型推导与 API 设计
类型推导驱动 API 设计
利用 TypeScript 的类型推导能力设计更智能的 API:
// 表单验证器 Builder 模式
interface ValidationRule<T> {
required?: boolean
min?: number
max?: number
pattern?: RegExp
custom?: (value: T) => string | null
}
class FormValidator<T extends Record<string, any>> {
private rules: Partial<Record<keyof T, ValidationRule<any>>> = {}
field<K extends keyof T>(
field: K,
rule: ValidationRule<T[K]>
): FormValidator<T> {
this.rules[field] = rule
return this
}
validate(data: Partial<T>): Record<keyof T, string | null> {
const errors = {} as Record<keyof T, string | null>
for (const [field, rule] of Object.entries(this.rules)) {
const value = data[field as keyof T]
const fieldRule = rule as ValidationRule<any>
if (fieldRule.required && (value === undefined || value === null || value === '')) {
errors[field as keyof T] = `${field} is required`
continue
}
if (fieldRule.min && typeof value === 'number' && value < fieldRule.min) {
errors[field as keyof T] = `${field} must be at least ${fieldRule.min}`
continue
}
if (fieldRule.pattern && typeof value === 'string' && !fieldRule.pattern.test(value)) {
errors[field as keyof T] = `${field} format is invalid`
continue
}
if (fieldRule.custom) {
const customError = fieldRule.custom(value)
if (customError) {
errors[field as keyof T] = customError
}
}
}
return errors
}
}
// 使用示例
interface LoginForm {
email: string
password: string
age: number
}
const validator = new FormValidator<LoginForm>()
.field('email', {
required: true,
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/
})
.field('password', {
required: true,
min: 6
})
.field('age', {
required: true,
min: 18,
max: 100
})
const errors = validator.validate({
email: 'invalid-email',
password: '123',
age: 16
})
// errors.email = "email format is invalid"
// errors.password = "password must be at least 6"
// errors.age = "age must be at least 18"
条件类型与 API 设计
使用条件类型创建更智能的 API:
// 根据字段类型自动生成验证规则
type FieldType<T> = T extends string
? 'string'
: T extends number
? 'number'
: T extends boolean
? 'boolean'
: 'unknown'
type ValidationRuleForField<T> = T extends string
? { required?: boolean; minLength?: number; maxLength?: number; pattern?: RegExp }
: T extends number
? { required?: boolean; min?: number; max?: number }
: T extends boolean
? { required?: boolean }
: {}
// 自动生成表单配置
function createFormConfig<T extends Record<string, any>>(
schema: T
): Record<keyof T, { type: FieldType<T[keyof T]>; rules: ValidationRuleForField<T[keyof T]> }> {
const config = {} as any
for (const [key, value] of Object.entries(schema)) {
const type = typeof value as FieldType<any>
config[key] = {
type,
rules: {} as ValidationRuleForField<any>
}
}
return config
}
// 使用
const userSchema = {
name: 'John',
age: 25,
isActive: true
}
const formConfig = createFormConfig(userSchema)
// formConfig.name.type = 'string'
// formConfig.age.type = 'number'
// formConfig.isActive.type = 'boolean'
🧪 实战练习
练习1:类型安全的 API 客户端
创建一个类型安全的 HTTP 客户端:
// 定义 API 响应类型
interface ApiResponse<T> {
data: T
status: number
message: string
}
// 定义 HTTP 方法
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'
// API 客户端类
class ApiClient {
private baseURL: string
constructor(baseURL: string) {
this.baseURL = baseURL
}
async request<T>(
method: HttpMethod,
endpoint: string,
data?: any
): Promise<ApiResponse<T>> {
const url = `${this.baseURL}${endpoint}`
const response = await fetch(url, {
method,
headers: {
'Content-Type': 'application/json'
},
body: data ? JSON.stringify(data) : undefined
})
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
return response.json()
}
get<T>(endpoint: string): Promise<ApiResponse<T>> {
return this.request<T>('GET', endpoint)
}
post<T>(endpoint: string, data: any): Promise<ApiResponse<T>> {
return this.request<T>('POST', endpoint, data)
}
put<T>(endpoint: string, data: any): Promise<ApiResponse<T>> {
return this.request<T>('PUT', endpoint, data)
}
delete<T>(endpoint: string): Promise<ApiResponse<T>> {
return this.request<T>('DELETE', endpoint)
}
}
// 使用示例
interface User {
id: number
name: string
email: string
}
const api = new ApiClient('https://api.example.com')
// 类型安全的 API 调用
const users = await api.get<User[]>('/users')
const user = await api.get<User>('/users/1')
const newUser = await api.post<User>('/users', { name: 'John', email: 'john@example.com' })
练习2:Vue 组件类型安全
创建一个类型安全的表单组件:
<!-- FormInput.vue -->
<template>
<div class="form-input">
<label v-if="label" :for="id">{{ label }}</label>
<input
:id="id"
:type="type"
:value="modelValue"
:placeholder="placeholder"
:disabled="disabled"
@input="handleInput"
@blur="handleBlur"
/>
<span v-if="error" class="error">{{ error }}</span>
</div>
</template>
<script setup lang="ts" generic="T extends string | number">
interface Props {
modelValue: T
type?: 'text' | 'email' | 'password' | 'number'
label?: string
placeholder?: string
disabled?: boolean
error?: string
}
interface Emits {
(e: 'update:modelValue', value: T): void
(e: 'blur'): void
}
const props = withDefaults(defineProps<Props>(), {
type: 'text'
})
const emit = defineEmits<Emits>()
const id = `input-${Math.random().toString(36).substr(2, 9)}`
const handleInput = (event: Event) => {
const target = event.target as HTMLInputElement
const value = props.type === 'number' ? Number(target.value) as T : target.value as T
emit('update:modelValue', value)
}
const handleBlur = () => {
emit('blur')
}
</script>
<style scoped>
.form-input {
margin-bottom: 1rem;
}
label {
display: block;
margin-bottom: 0.5rem;
font-weight: 500;
}
input {
width: 100%;
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 4px;
}
input:focus {
outline: none;
border-color: #42b883;
}
.error {
color: #e74c3c;
font-size: 0.875rem;
margin-top: 0.25rem;
}
</style>
练习3:类型安全的 Vuex/Pinia Store
创建一个类型安全的 Pinia store:
// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
interface User {
id: number
name: string
email: string
avatar?: string
}
interface LoginCredentials {
email: string
password: string
}
interface RegisterData {
name: string
email: string
password: string
}
export const useUserStore = defineStore('user', () => {
// State
const user = ref<User | null>(null)
const loading = ref(false)
const error = ref<string | null>(null)
// Getters
const isLoggedIn = computed(() => !!user.value)
const userName = computed(() => user.value?.name || 'Guest')
// Actions
const login = async (credentials: LoginCredentials): Promise<void> => {
loading.value = true
error.value = null
try {
// 模拟 API 调用
const response = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(credentials)
})
if (!response.ok) {
throw new Error('Login failed')
}
const userData = await response.json()
user.value = userData
} catch (err) {
error.value = err instanceof Error ? err.message : 'Login failed'
throw err
} finally {
loading.value = false
}
}
const register = async (data: RegisterData): Promise<void> => {
loading.value = true
error.value = null
try {
const response = await fetch('/api/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
})
if (!response.ok) {
throw new Error('Registration failed')
}
const userData = await response.json()
user.value = userData
} catch (err) {
error.value = err instanceof Error ? err.message : 'Registration failed'
throw err
} finally {
loading.value = false
}
}
const logout = (): void => {
user.value = null
error.value = null
}
const updateProfile = async (updates: Partial<User>): Promise<void> => {
if (!user.value) return
loading.value = true
error.value = null
try {
const response = await fetch(`/api/users/${user.value.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updates)
})
if (!response.ok) {
throw new Error('Update failed')
}
const updatedUser = await response.json()
user.value = updatedUser
} catch (err) {
error.value = err instanceof Error ? err.message : 'Update failed'
throw err
} finally {
loading.value = false
}
}
return {
// State
user,
loading,
error,
// Getters
isLoggedIn,
userName,
// Actions
login,
register,
logout,
updateProfile
}
})
📚 最佳实践
1. 类型定义组织
// types/index.ts - 全局类型定义
export interface User {
id: number
name: string
email: string
}
export interface ApiResponse<T> {
data: T
status: number
message: string
}
// types/api.ts - API 相关类型
export interface LoginRequest {
email: string
password: string
}
export interface LoginResponse {
user: User
token: string
}
// types/components.ts - 组件相关类型
export interface ButtonProps {
variant: 'primary' | 'secondary' | 'danger'
size: 'small' | 'medium' | 'large'
disabled?: boolean
}
2. 类型导入规范
// 使用 type 导入类型
import type { User, ApiResponse } from '@/types'
import type { ButtonProps } from '@/components/Button.vue'
// 混合导入
import { ref, computed, type Ref } from 'vue'
import { useRouter, type Router } from 'vue-router'
3. 类型断言最佳实践
// ✅ 好的做法:使用类型守卫
function isUser(obj: any): obj is User {
return obj && typeof obj.id === 'number' && typeof obj.name === 'string'
}
if (isUser(data)) {
// data 现在是 User 类型
console.log(data.name)
}
// ✅ 好的做法:使用可选链和空值合并
const userName = user?.name ?? 'Unknown'
// ❌ 避免:过度使用类型断言
const user = data as User // 不安全
🚨 常见问题
1. 类型错误但代码能运行
问题:TypeScript 报错但 JavaScript 代码正常 解决:检查 tsconfig.json
配置,确保启用了严格模式
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true
}
}
2. Vue 组件类型推断问题
问题:Vue 组件中类型推断不准确 解决:使用 <script setup lang="ts" generic="T">
语法
3. 第三方库类型问题
问题:第三方库没有类型定义 解决:安装 @types/package-name
或创建类型声明文件
// types/global.d.ts
declare module 'some-library' {
export function someFunction(): void
}
📖 延伸阅读
下一章预告:我们将深入学习现代 CSS 能力,包括 Flexbox、Grid 布局、CSS 变量、容器查询等现代特性,为构建响应式和可维护的样式系统打下基础。