摘要:TypeScript 的类型系统,表面上是"给 JavaScript 加个类型",实际上是一门图灵完备的编程语言。本文将带你从"any 大法好"进化到"类型体操运动员",用 15 个实战案例,让你的代码从"能跑就行"升级到"类型安全の艺术品"。警告:学完可能会对 any 产生生理性不适。
引言:一个关于类型的"真香"故事
三个月前的我:
typescript
// 管他什么类型,any 一把梭!
const data: any = await fetchData()
const result: any = processData(data)
// 能跑就行,类型是什么?能吃吗?
三个月后的我:
typescript
// 这类型推导,这自动补全,这编译时检查...真香!
const data = await fetchData<UserResponse>()
const result = processData(data) // 完美的类型推导
// IDE 告诉我 result.user.name 是 string,爽!
转变的契机?
一个深夜,线上报错:Cannot read property 'name' of undefined
排查了 3 小时,发现是后端改了接口字段名,前端没同步。
如果当时用了正确的类型定义,TypeScript 编译时就会告诉我这个问题。
从那以后,我开始认真学习 TypeScript 的类型系统。
今天,我要把这些"类型体操"的技巧分享给你。
第一章:热身运动 ------ 内置工具类型
TypeScript 内置了很多实用的工具类型,先来热热身。
1.1 Partial ------ 让所有属性变成可选
场景: 更新用户信息时,不需要传所有字段。
typescript
interface User {
id: number
name: string
email: string
age: number
avatar: string
}
// ❌ 不用 Partial:要传所有字段
function updateUser(id: number, user: User) {
// ...
}
updateUser(1, { id: 1, name: "张三", email: "...", age: 25, avatar: "..." })
// ✅ 用 Partial:只传需要更新的字段
function updateUserBetter(id: number, updates: Partial<User>) {
// ...
}
updateUserBetter(1, { name: "李四" }) // 只更新名字,完美!
// Partial 的实现原理(面试常考)
type MyPartial<T> = {
[P in keyof T]?: T[P]
}
1.2 Required ------ Partial 的反义词
场景: 确保配置对象的所有字段都被填写。
typescript
interface Config {
apiUrl?: string
timeout?: number
retries?: number
}
// 用户可以只传部分配置
const userConfig: Config = { apiUrl: "https://api.example.com" }
// 但内部处理时,需要确保所有字段都有值
function initApp(config: Required<Config>) {
console.log(config.timeout) // 类型是 number,不是 number | undefined
}
// 合并默认配置后调用
const defaultConfig: Required<Config> = {
apiUrl: "https://default.api.com",
timeout: 5000,
retries: 3,
}
initApp({ ...defaultConfig, ...userConfig })
// Required 的实现原理
type MyRequired<T> = {
[P in keyof T]-?: T[P] // -? 移除可选标记
}
1.3 Pick<T, K> ------ 从类型中挑选部分属性
场景: 列表页只需要显示用户的部分信息。
typescript
interface User {
id: number
name: string
email: string
password: string // 敏感信息
createdAt: Date
updatedAt: Date
}
// 列表页只需要这些字段
type UserListItem = Pick<User, "id" | "name" | "email">
// 等价于:
// type UserListItem = {
// id: number;
// name: string;
// email: string;
// }
const users: UserListItem[] = [
{ id: 1, name: "张三", email: "zhang@example.com" },
// password 不会出现,类型安全!
]
// Pick 的实现原理
type MyPick<T, K extends keyof T> = {
[P in K]: T[P]
}
1.4 Omit<T, K> ------ Pick 的反义词
场景: 创建用户时,不需要传 id(由数据库生成)。
typescript
interface User {
id: number
name: string
email: string
createdAt: Date
}
// 创建用户时排除 id 和 createdAt
type CreateUserInput = Omit<User, "id" | "createdAt">
// 等价于:
// type CreateUserInput = {
// name: string;
// email: string;
// }
function createUser(input: CreateUserInput): User {
return {
...input,
id: generateId(),
createdAt: new Date(),
}
}
// Omit 的实现原理
type MyOmit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>
1.5 Record<K, V> ------ 快速创建对象类型
场景: 创建一个以字符串为键、特定类型为值的对象。
typescript
// 状态映射
type Status = "pending" | "success" | "error"
interface StatusInfo {
label: string
color: string
icon: string
}
const statusMap: Record<Status, StatusInfo> = {
pending: { label: "处理中", color: "orange", icon: "⏳" },
success: { label: "成功", color: "green", icon: "✅" },
error: { label: "失败", color: "red", icon: "❌" },
}
// 如果漏写了某个状态,TypeScript 会报错!
// const badStatusMap: Record<Status, StatusInfo> = {
// pending: { ... },
// success: { ... },
// // 缺少 error,报错!
// };
// Record 的实现原理
type MyRecord<K extends keyof any, V> = {
[P in K]: V
}
第二章:进阶体操 ------ 条件类型与推断
2.1 条件类型基础
条件类型就像类型系统里的 if-else:
typescript
// 基本语法:T extends U ? X : Y
// 如果 T 可以赋值给 U,则结果是 X,否则是 Y
type IsString<T> = T extends string ? true : false
type A = IsString<string> // true
type B = IsString<number> // false
type C = IsString<"hello"> // true(字面量类型也是 string)
// 实际应用:根据输入类型返回不同的输出类型
type ApiResponse<T> = T extends "user"
? { id: number; name: string }
: T extends "product"
? { id: number; price: number }
: never
type UserResponse = ApiResponse<"user"> // { id: number; name: string }
type ProductResponse = ApiResponse<"product"> // { id: number; price: number }
2.2 infer 关键字 ------ 类型推断の魔法
infer 可以在条件类型中"捕获"类型:
typescript
// 获取函数的返回类型
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never
function getUser() {
return { id: 1, name: "张三" }
}
type UserType = MyReturnType<typeof getUser>
// { id: number; name: string }
// 获取函数的参数类型
type MyParameters<T> = T extends (...args: infer P) => any ? P : never
function login(username: string, password: string, remember?: boolean) {
// ...
}
type LoginParams = MyParameters<typeof login>
// [username: string, password: string, remember?: boolean]
// 获取 Promise 的解析类型
type MyAwaited<T> = T extends Promise<infer U> ? MyAwaited<U> : T
type A = MyAwaited<Promise<string>> // string
type B = MyAwaited<Promise<Promise<number>>> // number(递归解析)
// 获取数组元素类型
type ElementType<T> = T extends (infer E)[] ? E : never
type C = ElementType<string[]> // string
type D = ElementType<number[]> // number
2.3 分布式条件类型
当条件类型作用于联合类型时,会自动"分发":
typescript
type ToArray<T> = T extends any ? T[] : never
// 联合类型会被分发处理
type Result = ToArray<string | number>
// 等价于 ToArray<string> | ToArray<number>
// 结果是 string[] | number[]
// 如果不想分发,用方括号包裹
type ToArrayNoDistribute<T> = [T] extends [any] ? T[] : never
type Result2 = ToArrayNoDistribute<string | number>
// 结果是 (string | number)[]
// 实际应用:过滤联合类型
type Exclude<T, U> = T extends U ? never : T
type Numbers = 1 | 2 | 3 | 4 | 5
type BigNumbers = Exclude<Numbers, 1 | 2> // 3 | 4 | 5
type Extract<T, U> = T extends U ? T : never
type OnlyStrings = Extract<string | number | boolean, string> // string
第三章:高级体操 ------ 模板字面量类型
TypeScript 4.1 引入的模板字面量类型,让类型操作更加灵活。
3.1 基础用法
typescript
// 字符串拼接
type Greeting = `Hello, ${string}!`
const a: Greeting = "Hello, World!" // ✅
const b: Greeting = "Hello, TypeScript!" // ✅
// const c: Greeting = 'Hi, World!'; // ❌ 不以 Hello 开头
// 联合类型展开
type Color = "red" | "green" | "blue"
type Size = "small" | "medium" | "large"
type ColorSize = `${Color}-${Size}`
// 'red-small' | 'red-medium' | 'red-large' |
// 'green-small' | 'green-medium' | 'green-large' |
// 'blue-small' | 'blue-medium' | 'blue-large'
// CSS 类名生成
type ButtonVariant = `btn-${Color}` | `btn-${Size}`
// 'btn-red' | 'btn-green' | 'btn-blue' | 'btn-small' | 'btn-medium' | 'btn-large'
3.2 字符串操作类型
typescript
// 内置的字符串操作类型
type Upper = Uppercase<"hello"> // 'HELLO'
type Lower = Lowercase<"HELLO"> // 'hello'
type Cap = Capitalize<"hello"> // 'Hello'
type Uncap = Uncapitalize<"Hello"> // 'hello'
// 实际应用:事件处理器命名
type EventName = "click" | "focus" | "blur"
type EventHandler = `on${Capitalize<EventName>}`
// 'onClick' | 'onFocus' | 'onBlur'
// 生成 getter/setter 方法名
type PropName = "name" | "age" | "email"
type Getter = `get${Capitalize<PropName>}` // 'getName' | 'getAge' | 'getEmail'
type Setter = `set${Capitalize<PropName>}` // 'setName' | 'setAge' | 'setEmail'
3.3 模板字面量 + infer = 字符串解析
typescript
// 解析路由参数
type ExtractRouteParams<T extends string> =
T extends `${infer _Start}:${infer Param}/${infer Rest}`
? Param | ExtractRouteParams<`/${Rest}`>
: T extends `${infer _Start}:${infer Param}`
? Param
: never
type Params = ExtractRouteParams<"/users/:userId/posts/:postId">
// 'userId' | 'postId'
// 解析查询字符串
type ParseQueryString<T extends string> =
T extends `${infer Key}=${infer Value}&${infer Rest}`
? { [K in Key]: Value } & ParseQueryString<Rest>
: T extends `${infer Key}=${infer Value}`
? { [K in Key]: Value }
: {}
type Query = ParseQueryString<"name=张三&age=25&city=北京">
// { name: '张三' } & { age: '25' } & { city: '北京' }
// 驼峰转短横线
type CamelToKebab<S extends string> = S extends `${infer First}${infer Rest}`
? First extends Uppercase<First>
? `-${Lowercase<First>}${CamelToKebab<Rest>}`
: `${First}${CamelToKebab<Rest>}`
: S
type Kebab = CamelToKebab<"backgroundColor"> // 'background-color'
type Kebab2 = CamelToKebab<"fontSize"> // 'font-size'
第四章:实战体操 ------ 真实场景应用
4.1 类型安全的事件系统
typescript
// 定义事件映射
interface EventMap {
"user:login": { userId: string; timestamp: number }
"user:logout": { userId: string }
"cart:add": { productId: string; quantity: number }
"cart:remove": { productId: string }
}
// 类型安全的事件发射器
class TypedEventEmitter<T extends Record<string, any>> {
private listeners: Map<keyof T, Set<Function>> = new Map()
on<K extends keyof T>(event: K, callback: (data: T[K]) => void): void {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set())
}
this.listeners.get(event)!.add(callback)
}
off<K extends keyof T>(event: K, callback: (data: T[K]) => void): void {
this.listeners.get(event)?.delete(callback)
}
emit<K extends keyof T>(event: K, data: T[K]): void {
this.listeners.get(event)?.forEach((cb) => cb(data))
}
}
// 使用
const emitter = new TypedEventEmitter<EventMap>()
// ✅ 类型安全:参数类型自动推导
emitter.on("user:login", (data) => {
console.log(data.userId) // string
console.log(data.timestamp) // number
})
// ✅ 类型安全:emit 时参数类型检查
emitter.emit("user:login", { userId: "123", timestamp: Date.now() })
// ❌ 类型错误:缺少 timestamp
// emitter.emit('user:login', { userId: '123' });
// ❌ 类型错误:事件名不存在
// emitter.emit('user:unknown', {});
4.2 类型安全的 API 客户端
typescript
// API 路由定义
interface ApiRoutes {
"GET /users": {
response: { id: number; name: string }[]
}
"GET /users/:id": {
params: { id: string }
response: { id: number; name: string; email: string }
}
"POST /users": {
body: { name: string; email: string }
response: { id: number; name: string; email: string }
}
"PUT /users/:id": {
params: { id: string }
body: { name?: string; email?: string }
response: { id: number; name: string; email: string }
}
"DELETE /users/:id": {
params: { id: string }
response: { success: boolean }
}
}
// 提取路由信息的工具类型
type ExtractMethod<T extends string> = T extends `${infer M} ${string}`
? M
: never
type ExtractPath<T extends string> = T extends `${string} ${infer P}`
? P
: never
// API 客户端类型
type ApiClient = {
[K in keyof ApiRoutes as Lowercase<ExtractMethod<K & string>>]: (
path: ExtractPath<K & string>,
options?: {
params?: ApiRoutes[K] extends { params: infer P } ? P : never
body?: ApiRoutes[K] extends { body: infer B } ? B : never
}
) => Promise<ApiRoutes[K]["response"]>
}
// 简化版实现
function createApiClient(baseUrl: string) {
const request = async (method: string, path: string, options?: any) => {
let url = `${baseUrl}${path}`
// 替换路径参数
if (options?.params) {
Object.entries(options.params).forEach(([key, value]) => {
url = url.replace(`:${key}`, String(value))
})
}
const response = await fetch(url, {
method,
headers: { "Content-Type": "application/json" },
body: options?.body ? JSON.stringify(options.body) : undefined,
})
return response.json()
}
return {
get: (path: string, options?: any) => request("GET", path, options),
post: (path: string, options?: any) => request("POST", path, options),
put: (path: string, options?: any) => request("PUT", path, options),
delete: (path: string, options?: any) => request("DELETE", path, options),
}
}
// 使用示例
const api = createApiClient("https://api.example.com")
// 类型推导正常工作
async function demo() {
const users = await api.get("/users")
// users 类型:{ id: number; name: string }[]
const user = await api.get("/users/:id", { params: { id: "123" } })
// user 类型:{ id: number; name: string; email: string }
const newUser = await api.post("/users", {
body: { name: "张三", email: "zhang@example.com" },
})
// newUser 类型:{ id: number; name: string; email: string }
}
4.3 深度只读类型
typescript
// 递归地将所有属性变为只读
type DeepReadonly<T> = T extends Function
? T
: T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T
interface Config {
api: {
baseUrl: string
timeout: number
headers: {
authorization: string
}
}
features: {
darkMode: boolean
notifications: boolean
}
}
type ReadonlyConfig = DeepReadonly<Config>
const config: ReadonlyConfig = {
api: {
baseUrl: "https://api.example.com",
timeout: 5000,
headers: {
authorization: "Bearer xxx",
},
},
features: {
darkMode: true,
notifications: false,
},
}
// ❌ 所有层级都不能修改
// config.api.baseUrl = 'xxx'; // 错误
// config.api.headers.authorization = 'yyy'; // 错误
// config.features.darkMode = false; // 错误
4.4 路径类型(获取对象的所有路径)
typescript
// 获取对象所有可能的路径
type Paths<T, Prefix extends string = ""> = T extends object
? {
[K in keyof T]: K extends string
? Prefix extends ""
? K | Paths<T[K], K>
: `${Prefix}.${K}` | Paths<T[K], `${Prefix}.${K}`>
: never
}[keyof T]
: never
interface User {
id: number
profile: {
name: string
address: {
city: string
street: string
}
}
settings: {
theme: string
}
}
type UserPaths = Paths<User>
// 'id' | 'profile' | 'profile.name' | 'profile.address' |
// 'profile.address.city' | 'profile.address.street' |
// 'settings' | 'settings.theme'
// 根据路径获取值类型
type PathValue<T, P extends string> = P extends `${infer Key}.${infer Rest}`
? Key extends keyof T
? PathValue<T[Key], Rest>
: never
: P extends keyof T
? T[P]
: never
type CityType = PathValue<User, "profile.address.city"> // string
type ProfileType = PathValue<User, "profile"> // { name: string; address: { city: string; street: string } }
// 类型安全的 get 函数
function get<T, P extends Paths<T>>(obj: T, path: P): PathValue<T, P> {
return path.split(".").reduce((acc: any, key) => acc?.[key], obj)
}
const user: User = {
id: 1,
profile: {
name: "张三",
address: { city: "北京", street: "长安街" },
},
settings: { theme: "dark" },
}
const city = get(user, "profile.address.city") // 类型是 string
const name = get(user, "profile.name") // 类型是 string
// const invalid = get(user, 'profile.invalid'); // ❌ 类型错误
第五章:类型体操の实用工具箱
5.1 NonNullable ------ 排除 null 和 undefined
typescript
type MaybeString = string | null | undefined
type DefinitelyString = NonNullable<MaybeString> // string
// 实现原理
type MyNonNullable<T> = T extends null | undefined ? never : T
// 实际应用:处理可选链的结果
interface User {
profile?: {
avatar?: string
}
}
function getAvatar(user: User): string {
const avatar = user.profile?.avatar
// avatar 类型是 string | undefined
if (avatar) {
// 这里 avatar 类型是 string
return avatar
}
return "/default-avatar.png"
}
5.2 类型守卫工具
typescript
// 创建类型守卫函数
function isString(value: unknown): value is string {
return typeof value === "string"
}
function isNumber(value: unknown): value is number {
return typeof value === "number"
}
function isNotNull<T>(value: T | null | undefined): value is T {
return value !== null && value !== undefined
}
// 数组过滤时保持类型
const mixedArray: (string | null | undefined)[] = [
"a",
null,
"b",
undefined,
"c",
]
// ❌ filter 后类型还是 (string | null | undefined)[]
const filtered1 = mixedArray.filter(
(item) => item !== null && item !== undefined
)
// ✅ 使用类型守卫,类型变成 string[]
const filtered2 = mixedArray.filter(isNotNull)
// filtered2 类型是 string[]
// 通用的类型守卫生成器
function createTypeGuard<T>(check: (value: unknown) => boolean) {
return (value: unknown): value is T => check(value)
}
interface User {
id: number
name: string
}
const isUser = createTypeGuard<User>(
(value): value is User =>
typeof value === "object" &&
value !== null &&
"id" in value &&
"name" in value
)
function processData(data: unknown) {
if (isUser(data)) {
// data 类型是 User
console.log(data.name)
}
}
5.3 函数重载の类型体操
typescript
// 根据参数类型返回不同的结果类型
function process(input: string): string[]
function process(input: number): number
function process(input: string | number): string[] | number {
if (typeof input === "string") {
return input.split("")
}
return input * 2
}
const a = process("hello") // string[]
const b = process(42) // number
// 使用条件类型实现类似效果
type ProcessResult<T> = T extends string
? string[]
: T extends number
? number
: never
function processGeneric<T extends string | number>(input: T): ProcessResult<T> {
if (typeof input === "string") {
return input.split("") as ProcessResult<T>
}
return (input * 2) as ProcessResult<T>
}
const c = processGeneric("hello") // string[]
const d = processGeneric(42) // number
5.4 Builder 模式の类型安全
typescript
// 类型安全的 Builder 模式
interface UserBuilder<T extends Partial<User> = {}> {
setName<N extends string>(name: N): UserBuilder<T & { name: N }>
setAge<A extends number>(age: A): UserBuilder<T & { age: A }>
setEmail<E extends string>(email: E): UserBuilder<T & { email: E }>
build(): T extends User ? User : never
}
interface User {
name: string
age: number
email: string
}
function createUserBuilder(): UserBuilder {
const data: Partial<User> = {}
return {
setName(name) {
data.name = name
return this as any
},
setAge(age) {
data.age = age
return this as any
},
setEmail(email) {
data.email = email
return this as any
},
build() {
if (!data.name || !data.age || !data.email) {
throw new Error("Missing required fields")
}
return data as User
},
}
}
// 使用
const user = createUserBuilder()
.setName("张三")
.setAge(25)
.setEmail("zhang@example.com")
.build()
// 如果漏掉某个字段,build() 返回 never,使用时会报错
5.5 状态机の类型安全
typescript
// 定义状态和转换
type OrderState = "pending" | "paid" | "shipped" | "delivered" | "cancelled"
// 定义合法的状态转换
type StateTransitions = {
pending: "paid" | "cancelled"
paid: "shipped" | "cancelled"
shipped: "delivered"
delivered: never
cancelled: never
}
// 类型安全的状态机
class OrderStateMachine<S extends OrderState> {
constructor(private state: S) {}
getState(): S {
return this.state
}
// 只允许合法的状态转换
transition<N extends StateTransitions[S]>(newState: N): OrderStateMachine<N> {
console.log(`状态从 ${this.state} 转换到 ${newState}`)
return new OrderStateMachine(newState)
}
}
// 使用
let order = new OrderStateMachine("pending")
// ✅ 合法转换
order = order.transition("paid")
order = order.transition("shipped")
order = order.transition("delivered")
// ❌ 非法转换(类型错误)
// order.transition('pending'); // delivered 不能转换到 pending
// 从头开始
const newOrder = new OrderStateMachine("pending")
// newOrder.transition('shipped'); // ❌ pending 不能直接到 shipped
newOrder.transition("paid") // ✅ pending -> paid
第六章:避坑指南
6.1 any vs unknown
typescript
// ❌ any:放弃类型检查
function badProcess(data: any) {
data.foo.bar.baz() // 不报错,但运行时可能崩溃
}
// ✅ unknown:安全的"任意类型"
function goodProcess(data: unknown) {
// data.foo; // ❌ 报错:unknown 类型不能直接访问属性
if (typeof data === "object" && data !== null && "foo" in data) {
// 现在可以安全访问
console.log((data as { foo: unknown }).foo)
}
}
6.2 类型断言の正确姿势
typescript
// ❌ 危险:强制断言可能导致运行时错误
const data = JSON.parse('{"name": "张三"}') as { name: string; age: number }
console.log(data.age.toFixed(2)) // 运行时错误!age 是 undefined
// ✅ 安全:使用类型守卫验证
interface User {
name: string
age: number
}
function isUser(data: unknown): data is User {
return (
typeof data === "object" &&
data !== null &&
"name" in data &&
"age" in data &&
typeof (data as User).name === "string" &&
typeof (data as User).age === "number"
)
}
const parsed = JSON.parse('{"name": "张三"}')
if (isUser(parsed)) {
console.log(parsed.age.toFixed(2)) // 类型安全
} else {
console.log("数据格式不正确")
}
6.3 泛型约束の常见错误
typescript
// ❌ 错误:T 可能没有 length 属性
function getLength<T>(item: T): number {
// return item.length; // 报错
return 0
}
// ✅ 正确:添加约束
function getLengthCorrect<T extends { length: number }>(item: T): number {
return item.length
}
getLengthCorrect("hello") // ✅ string 有 length
getLengthCorrect([1, 2, 3]) // ✅ array 有 length
// getLengthCorrect(123); // ❌ number 没有 length
6.4 索引签名の陷阱
typescript
interface StringMap {
[key: string]: string
}
const map: StringMap = {
name: "张三",
age: "25", // 必须是 string,不能是 number
}
// 陷阱:访问不存在的键不会报错
const value = map.nonExistent // 类型是 string,但实际是 undefined!
// 解决方案:使用 Record 或更精确的类型
type SafeMap = Record<"name" | "age", string>
const safeMap: SafeMap = {
name: "张三",
age: "25",
}
// safeMap.nonExistent; // ❌ 报错:属性不存在
写在最后:类型体操の哲学
学习 TypeScript 类型体操,不是为了炫技,而是为了:
1. 编译时发现错误
- 类型错误在编译时就能发现,不用等到线上报错
- 重构时 IDE 会告诉你哪些地方需要修改
2. 更好的开发体验
- 自动补全更智能
- 文档就在类型里,不用翻文档
3. 代码即文档
- 类型定义本身就是最好的文档
- 新人看类型就知道怎么用
最后,送你一句话:
"类型不是枷锁,而是翅膀。"
当你的类型系统足够强大时,很多 bug 在你写代码的时候就已经被消灭了。
💬 互动时间:你在项目中用过哪些类型体操技巧?遇到过什么类型难题?评论区分享一下,一起探讨!
觉得这篇文章有用?点赞 + 在看 + 转发,让更多 TypeScript 开发者告别 any!
本文作者是一个从 "any 大法好" 进化到 "类型体操运动员" 的前端开发。关注我,一起在类型的世界里优雅地编程。