TypeScript 最佳实践 2025:从类型安全到开发效率
经过多年的 TypeScript 项目实战,我总结了一套真正能提升代码质量和开发效率的实践。这不是教科书式的规则,而是来自实际项目中的经验教训。
1. 类型设计:从数据流开始思考
优先使用 Type 而非 Interface
虽然两者在大多数情况下可以互换,但我更倾向于使用 type:
// ✅ 推荐:使用 type
type User = {
id: string
name: string
email: string
}
// 联合类型、交叉类型更直观
type Admin = User & { role: 'admin'; permissions: string[] }
type Guest = { sessionId: string }
type AppUser = Admin | Guest
// ❌ 使用 interface 会更繁琐
interface Admin extends User {
role: 'admin'
permissions: string[]
}
何时使用 Interface?
- 需要声明合并(Declaration Merging)时
- 定义类的契约时
善用 Discriminated Unions
这是 TypeScript 最强大的特性之一:
type LoadingState = {
status: 'loading'
}
type SuccessState<T> = {
status: 'success'
data: T
}
type ErrorState = {
status: 'error'
error: Error
}
type AsyncState<T> = LoadingState | SuccessState<T> | ErrorState
// 使用时,TypeScript 会自动收窄类型
function renderUser(state: AsyncState<User>) {
switch (state.status) {
case 'loading':
return <Spinner />
case 'success':
// TypeScript 知道这里 state.data 存在
return <UserProfile user={state.data} />
case 'error':
// TypeScript 知道这里 state.error 存在
return <ErrorMessage error={state.error} />
}
}
这比使用可选属性好得多:
// ❌ 不好:需要到处检查
type AsyncState<T> = {
loading?: boolean
data?: T
error?: Error
}
function renderUser(state: AsyncState<User>) {
if (state.loading) return <Spinner />
if (state.error) return <ErrorMessage error={state.error} />
if (state.data) return <UserProfile user={state.data} />
// 还要处理都不存在的情况?
}
2. 减少类型断言,增加类型推断
让 TypeScript 为你工作
// ❌ 过度使用类型断言
const data = JSON.parse(response) as User
const element = document.getElementById('app') as HTMLDivElement
// ✅ 使用类型守卫
function isUser(data: unknown): data is User {
return (
typeof data === 'object' &&
data !== null &&
'id' in data &&
'name' in data &&
'email' in data
)
}
const data = JSON.parse(response)
if (isUser(data)) {
// TypeScript 知道 data 是 User
console.log(data.email)
}
// ✅ 使用泛型
function getElementById<T extends HTMLElement>(id: string): T | null {
return document.getElementById(id) as T | null
}
const app = getElementById<HTMLDivElement>('app')
利用 const assertions
// ❌ 类型过于宽泛
const routes = {
home: '/',
about: '/about',
blog: '/blog'
}
// routes.home 的类型是 string
// ✅ 使用 const assertion
const routes = {
home: '/',
about: '/about',
blog: '/blog'
} as const
// routes.home 的类型是 '/'
// 现在可以创建精确的类型
type Route = typeof routes[keyof typeof routes] // '/' | '/about' | '/blog'
3. 泛型:强大但要克制
好的泛型使用
// ✅ 清晰的泛型约束
function mapObject<T extends Record<string, any>, U>(
obj: T,
mapper: (value: T[keyof T], key: keyof T) => U
): Record<keyof T, U> {
const result = {} as Record<keyof T, U>
for (const key in obj) {
result[key] = mapper(obj[key], key)
}
return result
}
// 使用
const user = { name: 'Alice', age: 30 }
const lengths = mapObject(user, v => String(v).length)
// lengths: { name: number, age: number }
避免过度泛型化
// ❌ 过度复杂
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]
}
// ✅ 使用简单的方案
type UpdateUserInput = Partial<Pick<User, 'name' | 'email'>>
4. Utility Types 的实战应用
TypeScript 内置了许多实用的工具类型,善用它们可以大大减少重复代码:
type User = {
id: string
name: string
email: string
password: string
createdAt: Date
updatedAt: Date
}
// 创建用户时不需要 id 和时间戳
type CreateUserInput = Omit<User, 'id' | 'createdAt' | 'updatedAt'>
// 更新用户时所有字段都是可选的
type UpdateUserInput = Partial<CreateUserInput>
// API 返回时不包含密码
type UserResponse = Omit<User, 'password'>
// 只读的用户数据
type ReadonlyUser = Readonly<User>
// 提取函数参数类型
async function createUser(input: CreateUserInput) {
// ...
}
type CreateUserParams = Parameters<typeof createUser>[0]
// 提取函数返回类型
type CreateUserResult = Awaited<ReturnType<typeof createUser>>
5. 严格模式配置
在 tsconfig.json 中启用所有严格检查:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"noFallthroughCasesInSwitch": true,
"forceConsistentCasingInFileNames": true,
"esModuleInterop": true,
"skipLibCheck": true
}
}
noUncheckedIndexedAccess 的重要性
const users = ['Alice', 'Bob']
// 没有 noUncheckedIndexedAccess
const user = users[10] // 类型是 string,实际是 undefined
// 启用后
const user = users[10] // 类型是 string | undefined
// 强制你处理边界情况
if (user) {
console.log(user.toUpperCase())
}
6. React + TypeScript 最佳实践
组件 Props 定义
// ✅ 推荐方式
type ButtonProps = {
children: React.ReactNode
variant?: 'primary' | 'secondary'
onClick?: () => void
} & Omit<React.ComponentPropsWithoutRef<'button'>, 'onClick'>
export function Button({ variant = 'primary', children, ...props }: ButtonProps) {
return (
<button className={`btn btn-${variant}`} {...props}>
{children}
</button>
)
}
// ❌ 避免使用 FC
const Button: React.FC<ButtonProps> = ({ children }) => {
// FC 有一些问题,比如隐式的 children
}
自定义 Hooks 类型
// ✅ 返回元组时使用 const assertion
function useToggle(initial = false) {
const [value, setValue] = useState(initial)
const toggle = useCallback(() => setValue(v => !v), [])
return [value, toggle] as const
// 返回类型是 [boolean, () => void] 而不是 (boolean | (() => void))[]
}
// ✅ 返回对象时更清晰
function useAsync<T>(asyncFunction: () => Promise<T>) {
const [state, setState] = useState<AsyncState<T>>({ status: 'loading' })
useEffect(() => {
asyncFunction()
.then(data => setState({ status: 'success', data }))
.catch(error => setState({ status: 'error', error }))
}, [])
return state
}
7. 类型体操的实际应用
有时候需要一些高级类型操作,但要确保它们真的有用:
// 从对象中提取特定类型的键
type PickByType<T, U> = {
[P in keyof T as T[P] extends U ? P : never]: T[P]
}
type User = {
id: string
name: string
age: number
isActive: boolean
}
type StringFields = PickByType<User, string> // { id: string; name: string }
type NumberFields = PickByType<User, number> // { age: number }
// 实际应用:自动生成表单字段类型
type FormFields<T> = {
[K in keyof T]: {
value: T[K]
error?: string
touched: boolean
}
}
type UserForm = FormFields<User>
// {
// id: { value: string; error?: string; touched: boolean }
// name: { value: string; error?: string; touched: boolean }
// ...
// }
8. 类型测试
是的,你应该测试你的类型!
// 使用 @ts-expect-error 验证类型错误
type Assert<T, Expected> = T extends Expected
? Expected extends T
? true
: never
: never
// 测试
type Test1 = Assert<PickByType<User, string>, { id: string; name: string }> // true
// @ts-expect-error - 应该报错
type Test2 = Assert<PickByType<User, string>, { id: string }> // 类型不匹配
// 或使用 expect-type 库
import { expectTypeOf } from 'expect-type'
expectTypeOf<PickByType<User, string>>().toEqualTypeOf<{ id: string; name: string }>()
9. 常见陷阱
枚举的问题
// ❌ 避免使用 enum
enum Color {
Red,
Green,
Blue
}
// 会生成运行时代码,增加包体积
// ✅ 使用 const object
const Color = {
Red: 'red',
Green: 'green',
Blue: 'blue'
} as const
type Color = typeof Color[keyof typeof Color]
可选链的类型陷阱
type User = {
address?: {
street: string
}
}
const user: User = {}
// ❌ 类型是 string | undefined,但运行时是 undefined
const street = user.address?.street
// ✅ 明确处理
const street = user.address?.street ?? 'Unknown'
10. 性能考虑
避免深度递归类型
// ❌ 可能导致编译性能问题
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object
? DeepReadonly<T[P]>
: T[P]
}
// ✅ 限制递归深度
type DeepReadonly<T, Depth extends number= 3> = {
readonly [P in keyof T]: Depth extends 0
? T[P]
: T[P] extends object
? DeepReadonly<T[P], Prev<Depth>>
: T[P]
}
结论
TypeScript 的价值不在于写更多的类型,而在于:
- 及早发现错误:编译时而非运行时
- 更好的 IDE 支持:智能提示和自动补全
- 自文档化:类型即文档
- 重构信心:类型系统保证修改的安全性
关键是找到类型安全和开发效率的平衡点。不要为了类型而类型,而是让类型为你的开发流程服务。
记住:最好的类型是那些你不需要写的类型 —— 让 TypeScript 的类型推断为你工作。
推荐资源
- TypeScript Handbook
- Type Challenges - 通过挑战学习高级类型
- ts-reset - 修复 TypeScript 的一些默认行为