Vue 3 + TypeScript 常用代码示例总结

一、TypeScript 内置工具类型

1.1 Partial​ 的作用

Partial<T>是 TypeScript 内置的工具类型,它可以将类型 T的所有属性变为可选。

php 复制代码
typescript
typescript
复制
// Partial 的实现原理(简化版)
type Partial<T> = {
  [P in keyof T]?: T[P];
};

// 使用示例
interface Theme {
  primaryColor: string;
  secondaryColor: string;
  fontSize: number;
}

// 正常 Theme 类型,所有属性都是必需的
const theme1: Theme = {
  primaryColor: '#1890ff',
  secondaryColor: '#52c41a',
  fontSize: 14
};
// ✓ 所有属性都必须提供

// 使用 Partial<Theme> 后,所有属性都变为可选
const theme2: Partial<Theme> = {
  primaryColor: '#1890ff'
  // secondaryColor 和 fontSize 可以不提供
};
// ✓ 可以只提供部分属性

// 在函数参数中使用 Partial
function updateTheme(theme: Partial<Theme>): void {
  // 可以只更新部分属性
  // theme 可能只包含 primaryColor,或只包含 fontSize,或都包含
}

// 调用示例
updateTheme({ primaryColor: '#ff4d4f' }); // ✓ 只更新一个属性
updateTheme({ fontSize: 16 }); // ✓ 只更新另一个属性
updateTheme({}); // ✓ 传递空对象也可以

1.2 其他常用工具类型

typescript 复制代码
typescript
typescript
复制
// 1. Required<T> - 将可选属性变为必填
interface User {
  id: number;
  name?: string;  // 可选
  email?: string; // 可选
}

type RequiredUser = Required<User>;
// 等价于:
// {
//   id: number;
//   name: string;   // 变为必填
//   email: string;  // 变为必填
// }

// 2. Readonly<T> - 将所有属性变为只读
type ReadonlyUser = Readonly<User>;
// 等价于:
// {
//   readonly id: number;
//   readonly name?: string;
//   readonly email?: string;
// }

// 3. Pick<T, K> - 从 T 中挑选部分属性
type UserBasicInfo = Pick<User, 'id' | 'name'>;
// 等价于:
// {
//   id: number;
//   name?: string;
// }

// 4. Omit<T, K> - 从 T 中排除部分属性
type UserWithoutId = Omit<User, 'id'>;
// 等价于:
// {
//   name?: string;
//   email?: string;
// }

// 5. Record<K, T> - 创建键值对类型
type UserMap = Record<string, User>;
// 等价于:
// {
//   [key: string]: User;
// }

// 6. Exclude<T, U> - 从 T 中排除可以赋值给 U 的类型
type T1 = 'a' | 'b' | 'c';
type T2 = 'a';
type Result = Exclude<T1, T2>; // 'b' | 'c'

// 7. Extract<T, U> - 从 T 中提取可以赋值给 U 的类型
type T3 = 'a' | 'b' | 'c';
type T4 = 'a' | 'd';
type Result2 = Extract<T3, T4>; // 'a'

// 8. NonNullable<T> - 排除 null 和 undefined
type T5 = string | number | null | undefined;
type Result3 = NonNullable<T5>; // string | number

二、Vue 3 中的类型

2.1 ComputedRef​ 类型

ComputedRef<T>是 Vue 3 中计算属性的类型,它是 Ref<T>的子类型。

typescript 复制代码
typescript
typescript
复制
import { ref, computed, Ref, ComputedRef } from 'vue'

// 1. 基础使用
const count = ref<number>(0); // Ref<number>
const doubleCount = computed(() => count.value * 2); // ComputedRef<number>

// 2. 显式类型声明
const doubleCount2: ComputedRef<number> = computed(() => count.value * 2);

// 3. 带 getter 和 setter 的计算属性
const fullName = computed<string>({
  get: () => `${firstName.value} ${lastName.value}`,
  set: (value: string) => {
    const [first, last] = value.split(' ')
    firstName.value = first
    lastName.value = last || ''
  }
});

// 4. 在函数参数中使用
function logComputedValue(computedValue: ComputedRef<any>): void {
  console.log(computedValue.value);
}

logComputedValue(doubleCount);

2.2 Vue 3 常用类型

typescript 复制代码
typescript
typescript
复制
import {
  Ref,           // 响应式引用类型
  ComputedRef,   // 计算属性类型
  UnwrapRef,     // 解包响应式类型
  MaybeRef,      // 可能是 Ref 或普通值
  MaybeRefOrGetter, // 可能是 Ref、getter 函数或普通值
  WritableComputedRef, // 可写的计算属性
  ShallowRef,    // 浅层 Ref
  ShallowReactive, // 浅层 reactive
  ToRefs,        // 将 reactive 转换为 refs
  ComponentPublicInstance, // 组件实例类型
  VNode,         // 虚拟节点类型
  Component      // 组件类型
} from 'vue'

// 1. Ref 类型
const countRef: Ref<number> = ref(0);

// 2. UnwrapRef - 获取 Ref 内部的类型
type CountType = UnwrapRef<typeof countRef>; // number

// 3. MaybeRef - 接受 Ref 或普通值
function useDouble(value: MaybeRef<number>): ComputedRef<number> {
  return computed(() => {
    // 判断是否是 Ref
    if (isRef(value)) {
      return value.value * 2
    }
    return value * 2
  });
}

// 或者使用 unref 工具函数
import { unref } from 'vue'

function useDouble2(value: MaybeRef<number>): ComputedRef<number> {
  return computed(() => unref(value) * 2);
}

// 4. ToRefs - 将 reactive 转换为多个 ref
interface State {
  count: number;
  name: string;
}

const state = reactive<State>({ count: 0, name: 'Vue' });
const stateRefs: ToRefs<State> = toRefs(state);
// 现在可以使用 stateRefs.count.value 和 stateRefs.name.value

三、实际应用示例

3.1 表单组件示例

typescript 复制代码
typescript
typescript
复制
import { defineComponent, reactive, computed, toRefs } from 'vue'

// 表单数据类型
interface FormData {
  username: string
  email: string
  age: number | null
  agree: boolean
}

// 表单验证规则类型
type ValidationRule = (value: any) => string | true

interface FormRules {
  [key: string]: ValidationRule | ValidationRule[]
}

export default defineComponent({
  setup() {
    // 表单数据
    const formData = reactive<FormData>({
      username: '',
      email: '',
      age: null,
      agree: false
    })
    
    // 表单验证规则
    const rules: FormRules = {
      username: [
        (value: string) => !!value || '用户名不能为空',
        (value: string) => value.length >= 3 || '用户名至少3个字符'
      ],
      email: [
        (value: string) => !!value || '邮箱不能为空',
        (value: string) => /.+@.+..+/.test(value) || '邮箱格式不正确'
      ],
      age: (value: number | null) => {
        if (value === null) return '年龄不能为空'
        if (value < 0) return '年龄不能为负数'
        if (value > 150) return '年龄不能超过150岁'
        return true
      }
    }
    
    // 表单验证状态
    const errors = reactive<Partial<Record<keyof FormData, string>>>({})
    
    // 验证单个字段
    const validateField = (field: keyof FormData): boolean => {
      const value = formData[field]
      const rule = rules[field]
      
      if (!rule) {
        delete errors[field]
        return true
      }
      
      const rulesArray = Array.isArray(rule) ? rule : [rule]
      
      for (const validate of rulesArray) {
        const result = validate(value)
        if (typeof result === 'string') {
          errors[field] = result
          return false
        }
      }
      
      delete errors[field]
      return true
    }
    
    // 验证整个表单
    const validateForm = (): boolean => {
      let isValid = true
      
      Object.keys(formData).forEach(field => {
        if (!validateField(field as keyof FormData)) {
          isValid = false
        }
      })
      
      return isValid
    }
    
    // 提交表单
    const submitForm = (): void => {
      if (!validateForm()) {
        console.log('表单验证失败')
        return
      }
      
      console.log('提交表单:', formData)
      // 这里可以调用 API
    }
    
    // 重置表单
    const resetForm = (): void => {
      Object.assign(formData, {
        username: '',
        email: '',
        age: null,
        agree: false
      })
      
      Object.keys(errors).forEach(key => {
        delete errors[key as keyof typeof errors]
      })
    }
    
    // 计算属性:表单是否有效
    const isFormValid = computed<boolean>(() => {
      return Object.keys(errors).length === 0 &&
        formData.username !== '' &&
        formData.email !== '' &&
        formData.age !== null &&
        formData.agree
    })
    
    return {
      // 使用 toRefs 保持响应性
      ...toRefs(formData),
      errors,
      isFormValid,
      validateField,
      validateForm,
      submitForm,
      resetForm
    }
  }
})

3.2 使用工具类型的通用函数

typescript 复制代码
typescript
typescript
复制
// utils/types.ts
// 自定义工具类型

// 1. 深度可选类型
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

// 2. 深度只读类型
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};

// 3. 可空类型
type Nullable<T> = T | null | undefined;

// 4. 提取函数返回类型
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

// 5. 提取函数参数类型
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;

// 6. 提取 Promise 的返回值类型
type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T;

// 使用示例
interface User {
  id: number;
  name: string;
  profile: {
    avatar: string;
    bio: string;
  };
  tags: string[];
}

// 深度可选
type PartialUser = DeepPartial<User>;
// 可以这样使用:
const user1: PartialUser = {
  name: '张三',
  profile: {
    avatar: 'avatar.jpg'
    // bio 可以不提供
  }
  // tags 可以不提供
};

// 深度只读
type ReadonlyUser = DeepReadonly<User>;
const user2: ReadonlyUser = {
  id: 1,
  name: '李四',
  profile: {
    avatar: 'avatar.jpg',
    bio: 'Hello'
  },
  tags: ['a', 'b']
};
// user2.profile.bio = 'World'; // ❌ 错误:不能修改只读属性

// 可空类型
let nullableString: Nullable<string> = 'Hello';
nullableString = null; // ✓
nullableString = undefined; // ✓

3.3 组合式函数的类型

typescript 复制代码
typescript
typescript
复制
// composables/useFetch.ts
import { ref, computed, Ref, ComputedRef } from 'vue'

// 请求状态类型
type FetchStatus = 'idle' | 'loading' | 'success' | 'error'

// 返回类型
interface UseFetchReturn<T> {
  data: Ref<T | null>
  error: Ref<string | null>
  status: Ref<FetchStatus>
  isLoading: ComputedRef<boolean>
  isSuccess: ComputedRef<boolean>
  isError: ComputedRef<boolean>
  execute: (url: string, options?: RequestInit) => Promise<void>
  reset: () => void
}

// 选项类型
interface UseFetchOptions {
  immediate?: boolean
  initialData?: any
}

export function useFetch<T = any>(
  initialUrl?: string,
  options: UseFetchOptions = {}
): UseFetchReturn<T> {
  const { immediate = false, initialData = null } = options
  
  const data = ref<T | null>(initialData) as Ref<T | null>
  const error = ref<string | null>(null)
  const status = ref<FetchStatus>('idle')
  
  const isLoading = computed(() => status.value === 'loading')
  const isSuccess = computed(() => status.value === 'success')
  const isError = computed(() => status.value === 'error')
  
  const execute = async (url: string, requestOptions?: RequestInit): Promise<void> => {
    status.value = 'loading'
    error.value = null
    
    try {
      const response = await fetch(url, requestOptions)
      
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`)
      }
      
      const result = await response.json()
      data.value = result
      status.value = 'success'
    } catch (err) {
      error.value = err instanceof Error ? err.message : '请求失败'
      status.value = 'error'
    }
  }
  
  const reset = (): void => {
    data.value = initialData
    error.value = null
    status.value = 'idle'
  }
  
  // 立即执行
  if (immediate && initialUrl) {
    execute(initialUrl)
  }
  
  return {
    data,
    error,
    status,
    isLoading,
    isSuccess,
    isError,
    execute,
    reset
  }
}

// 在组件中使用
import { defineComponent, onMounted } from 'vue'

interface Post {
  id: number
  title: string
  body: string
  userId: number
}

export default defineComponent({
  setup() {
    // 使用 useFetch
    const { 
      data: posts, 
      error, 
      isLoading, 
      isSuccess, 
      execute 
    } = useFetch<Post[]>('https://jsonplaceholder.typicode.com/posts')
    
    // 或者延迟执行
    const { 
      data: user, 
      execute: fetchUser 
    } = useFetch<Post>(undefined, { immediate: false })
    
    onMounted(() => {
      // 手动执行
      fetchUser('https://jsonplaceholder.typicode.com/posts/1')
    })
    
    // 重新获取
    const refresh = (): void => {
      execute('https://jsonplaceholder.typicode.com/posts')
    }
    
    return {
      posts,
      error,
      isLoading,
      isSuccess,
      refresh
    }
  }
})

四、常见问题解答

Q1: 什么时候用 Partial

A: ​ 当你需要创建一个对象,它包含原始类型的一部分属性时使用。

typescript 复制代码
typescript
typescript
复制
// 更新用户信息时,通常只需要更新部分字段
interface User {
  id: number
  name: string
  email: string
  age: number
}

function updateUser(userId: number, updates: Partial<User>): void {
  // updates 可以只包含 name,或只包含 email,或任意组合
  // 但不会包含不存在的属性
}

Q2: ComputedRef和普通 Ref有什么区别?

A: ​ 主要区别:

  • ComputedRef是只读的,你不能直接修改它的值
  • ComputedRef的值是通过计算得到的
  • 你可以为 ComputedRef提供 setter,但通常不建议
javascript 复制代码
typescript
typescript
复制
// Ref - 可以直接修改
const count = ref(0)
count.value = 1  // ✓ 可以

// ComputedRef - 默认只读
const double = computed(() => count.value * 2)
double.value = 4  // ❌ 错误:不能直接修改计算属性

// 带 setter 的 ComputedRef
const fullName = computed({
  get: () => `${firstName.value} ${lastName.value}`,
  set: (value) => {
    const [first, last] = value.split(' ')
    firstName.value = first
    lastName.value = last || ''
  }
})

Q3: 什么时候需要显式指定类型?

A: ​ TypeScript 有类型推断,但以下情况建议显式指定:

typescript 复制代码
typescript
typescript
复制
// 1. 函数参数和返回值
function add(a: number, b: number): number {
  return a + b
}

// 2. 复杂对象
interface Config {
  apiUrl: string
  timeout: number
  retry: boolean
}

const config: Config = {
  apiUrl: '/api',
  timeout: 5000,
  retry: true
}

// 3. 组件 Props
interface Props {
  title: string
  count: number
  items: Array<{ id: number; name: string }>
}

// 4. API 响应
interface ApiResponse<T = any> {
  code: number
  data: T
  message: string
}

async function fetchUser(id: number): Promise<ApiResponse<User>> {
  const response = await fetch(`/api/users/${id}`)
  return response.json()
}

记住:TypeScript 的核心价值在于类型安全,良好的类型定义能帮助你在编码阶段就发现潜在的错误,提高代码质量和开发效率。

相关推荐
前端付豪2 小时前
实现多角色模式切换
前端·架构
从文处安2 小时前
「九九八十一难」从回调地狱到异步秩序:深入理解 JavaScript Promise
前端·javascript
要换昵称了2 小时前
Axios二次封装及API 调用框架
前端·vue.js
猫腻前端2 小时前
深度图d3绘制交互逻辑
前端
搞个锤子哟2 小时前
el-popover气泡宽度由内容撑起
前端
angerdream2 小时前
最新版vue3+TypeScript开发入门到实战教程之Pinia详解
前端·javascript·vue.js
没想好d2 小时前
通用管理后台组件库-14-图表和富文本组件
前端
siger2 小时前
前端部署缓存策略实践
前端·nginx
Mh2 小时前
react 设计哲学 | 严格模式
前端·react.js·preact