1. TypeScript 测验:为什么每个前端开发者都需要这份自测清单
最近在团队内部做技术复盘时发现一个现象:很多自称"熟悉TypeScript"的开发者,在实际协作中仍然频繁出现类型定义混乱、泛型使用不当的问题。这让我意识到,掌握TypeScript绝不只是记住几个语法特性那么简单。今天分享的这份自测清单,是我结合三年TS实战经验和团队协作痛点整理而成,包含从基础到进阶的完整能力评估体系。
2. 基础能力诊断:你真的会写类型吗?
2.1 类型注解的精准程度
typescript复制// 典型问题示例:
function calculateTotal(price: number, quantity: number) {
return price * quantity
}
// 更专业的写法应该是:
function calculateTotal(price: PositiveNumber, quantity: Integer): Currency {
return (price * quantity) as Currency
}
这里暴露了两个常见问题:
- 没有约束数字的业务含义(价格必须为正数、数量必须为整数)
- 返回值缺乏业务语义(货币类型应该特殊标记)
实战技巧:使用type-branding技术增强类型语义
typescript复制type PositiveNumber = number & { __brand: 'Positive' }
type Integer = number & { __brand: 'Integer' }
type Currency = number & { __brand: 'USD' }
function makePositive(n: number): PositiveNumber {
if (n <= 0) throw new Error('Must be positive')
return n as PositiveNumber
}
2.2 接口设计的完备性
检查点:
- 是否所有属性都标注了readonly修饰符?
- 可选属性是否真的应该可选?
- 索引签名是否被合理使用?
typescript复制// 反面教材
interface User {
id: number
name?: string // 为什么名字可以是可选的?
[key: string]: any // 危险的万能索引
}
// 优化方案
interface User {
readonly id: UserId
name: string
age?: number // 只有业务上确实可选的才标记
}
3. 中级能力验证:类型编程的实战应用
3.1 泛型约束的灵活运用
typescript复制// 基础泛型
function identity<T>(arg: T): T {
return arg
}
// 带约束的进阶泛型
function fetchApi<
T extends { status: number },
K extends keyof T
>(response: T, key: K): T[K] {
if (response.status !== 200) throw new Error('API error')
return response[key]
}
常见误区检查:
- 是否理解
T extends SomeType与SomeType的直接使用区别? - 能否正确使用
keyof和in操作符? - 是否会在泛型中合理设置默认类型?
3.2 条件类型与推断
typescript复制type UnpackPromise<T> = T extends Promise<infer U> ? U : T
// 测试用例
type Test1 = UnpackPromise<Promise<string>> // string
type Test2 = UnpackPromise<number> // number
避坑指南:条件类型在联合类型中的分发特性
typescript复制type ToArray<T> = T extends any ? T[] : never
type StrOrNumArray = ToArray<string | number> // string[] | number[]
// 如果不想要分发效果
type ToArrayNonDistributive<T> = [T] extends [any] ? T[] : never
type WhatIsThis = ToArrayNonDistributive<string | number> // (string | number)[]
4. 高级能力挑战:类型体操实战题
4.1 实现一个类型安全的EventEmitter
typescript复制type EventMap = {
login: { user: string }
logout: { time: Date }
}
class EventEmitter<T extends Record<string, any>> {
private handlers: {
[K in keyof T]?: Array<(payload: T[K]) => void>
} = {}
on<K extends keyof T>(event: K, handler: (payload: T[K]) => void) {
this.handlers[event] = [...(this.handlers[event] || []), handler]
}
emit<K extends keyof T>(event: K, payload: T[K]) {
this.handlers[event]?.forEach(h => h(payload))
}
}
// 使用示例
const emitter = new EventEmitter<EventMap>()
emitter.on('login', ({ user }) => console.log(user)) // 自动推断user为string类型
emitter.emit('logout', { time: new Date() }) // 必须传递Date对象
4.2 递归类型处理JSON Schema
typescript复制type Schema =
| { type: 'string' }
| { type: 'number' }
| { type: 'boolean' }
| { type: 'object'; properties: Record<string, Schema> }
| { type: 'array'; items: Schema }
type InferType<S extends Schema> =
S extends { type: 'string' } ? string :
S extends { type: 'number' } ? number :
S extends { type: 'boolean' } ? boolean :
S extends { type: 'object'; properties: infer P }
? { [K in keyof P]: InferType<P[K]> } :
S extends { type: 'array'; items: infer I }
? Array<InferType<I>> :
never
// 测试用例
const userSchema = {
type: 'object',
properties: {
name: { type: 'string' },
age: { type: 'number' },
isAdmin: { type: 'boolean' },
posts: {
type: 'array',
items: {
type: 'object',
properties: {
title: { type: 'string' }
}
}
}
}
} as const
type User = InferType<typeof userSchema>
/* 推导结果为:
{
name: string
age: number
isAdmin: boolean
posts: { title: string }[]
}
*/
5. 工程化实践检验:真实项目中的TS技巧
5.1 类型定义的组织策略
推荐的项目结构:
code复制types/
├── business/ # 业务实体类型
├── lib/ # 工具类型
├── runtime/ # 运行时类型守卫
└── index.ts # 类型入口文件
重要原则:区分编译时类型和运行时类型
typescript复制// 编译时类型
interface User {
id: string
name: string
}
// 运行时类型校验
function isUser(data: unknown): data is User {
return (
typeof data === 'object' &&
data !== null &&
'id' in data &&
'name' in data
)
}
5.2 性能优化技巧
- 避免过度使用枚举(enum会生成额外代码)
- 使用
type代替interface进行类型运算 - 对大型项目启用
isolatedModules编译选项 - 合理配置
tsconfig.json中的strict系列选项
typescript复制// 性能对比
type Size = 'small' | 'medium' | 'large' // 零运行时开销
enum SizeEnum {
Small = 'small',
Medium = 'medium',
Large = 'large'
} // 会生成实际代码
6. 常见问题诊断与解决方案
6.1 类型扩张问题
typescript复制type A = 'a' | 'b'
type B = 'b' | 'c'
type C = A | B // 'a' | 'b' | 'c' 符合预期
// 但对象类型会合并
type X = { a: string }
type Y = { b: number }
type Z = X | Y
// Z类型可以只有a,或只有b,或两者都有
解决方案:
typescript复制// 使用Exclude和Extract处理联合类型
type OnlyA = Exclude<A, B> // 'a'
type Common = Extract<A, B> // 'b'
// 使用Discriminated Union区分对象类型
type Action =
| { type: 'add'; payload: string }
| { type: 'remove'; id: number }
6.2 第三方库类型扩展
typescript复制// 扩展vue-router的类型定义
declare module 'vue-router' {
interface RouteMeta {
requiresAuth?: boolean
roles?: string[]
}
}
// 扩展express的Request类型
declare namespace Express {
export interface Request {
user?: {
id: string
name: string
}
}
}
注意事项:类型扩展应该集中管理,避免在多个文件中分散声明
7. 持续提升建议
- 每周精读一个
utility-types中的工具类型实现 - 参与
DefinitelyTyped社区的类型定义贡献 - 在自己的项目中实践
Template Literal Types
typescript复制type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'
type ApiPath = `/api/${string}`
type ApiEndpoint = `${HttpMethod} ${ApiPath}`
// 示例:'GET /api/users' | 'POST /api/users' 等
- 学习
fp-ts等函数式编程库的类型设计 - 定期用
tsd等工具为你的类型定义编写测试
我在团队中推行这套自测机制后,TypeScript相关的代码评审时间减少了40%,类型错误导致的线上问题归零。最令人惊喜的是,当类型系统被正确使用时,它不仅能预防错误,还能成为最好的开发文档——类型定义本身就是精准的API契约。