HiHuo
首页
博客
手册
工具
首页
博客
手册
工具
  • Vue & CSS 进阶

    • 第0章:工具链与开发体验
    • 第1章:TypeScript 核心
    • 第2章:CSS 现代能力
    • 第3章:Vue3 核心与原理
    • 第4章:路由与状态管理
    • 第5章:工程化与性能
    • 第6章:测试与质量
    • 第7章:CSS 进阶专题
    • 第8章:项目实战A - SaaS仪表盘
    • 第9章:项目实战B - 内容社区
    • 第10章:Vue 内核深入
    • 第11章:微前端与部署
    • CSS 变量体系与主题系统深度指南
    • 前端安全与防护专题
    • CSS 专题:动效优化与微交互
    • CSS 专题:图片与图形处理
    • 实时通信与WebSocket专题
    • 开发工具与调试技巧专题
    • 新兴技术与未来趋势专题
    • 移动端开发与PWA专题
    • 附录A:代码片段库
    • 附录B:练习题集

第1章:TypeScript 核心

TypeScript 是 Vue 的强力外挂,掌握 TS 的 20% 核心特性就能覆盖 80% 的开发场景。本章将深入讲解类型系统、泛型、条件类型等高级特性,并结合 Vue 实际应用场景。

📋 本章内容

  • 基础类型系统
  • 高级类型特性
  • Vue 中的 TypeScript
  • 类型推导与 API 设计
  • 实战练习

🎯 学习目标

  • 掌握 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
}

📖 延伸阅读

  • TypeScript 官方文档
  • Vue 3 TypeScript 支持
  • TypeScript 高级类型
  • Vue 3 Composition API 类型

下一章预告:我们将深入学习现代 CSS 能力,包括 Flexbox、Grid 布局、CSS 变量、容器查询等现代特性,为构建响应式和可维护的样式系统打下基础。

Prev
第0章:工具链与开发体验
Next
第2章:CSS 现代能力