一、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 的核心价值在于类型安全,良好的类型定义能帮助你在编码阶段就发现潜在的错误,提高代码质量和开发效率。