实战集成
本章将 TypeScript 的类型系统应用到日常开发的主流场景:React 组件与 Hooks 的类型标注、Node.js 服务端开发的类型收窄,以及 API 封装、错误处理等通用模式。
React + TypeScript
组件类型
函数组件有两种常用标注方式:
interface GreetingProps {
name: string
age?: number
}
// 方式一:React.FC(隐式包含 children)
const Greeting: React.FC<GreetingProps> = ({ name, age }) => (
<p>Hello, {name}{age && `, ${age}`}</p>
)
// 方式二:直接标注返回值(不隐式包含 children)
const Greeting2 = ({ name }: GreetingProps): JSX.Element => (
<p>Hello, {name}</p>
)🚨 陷阱:
React.FC隐式包含children?: ReactNode。如果你的组件不需要 children,使用方式二标注JSX.Element返回值更安全。
PropsWithChildren 可显式声明需要 children 的组件:
import { PropsWithChildren } from "react"
interface CardProps {
title: string
}
const Card: React.FC<CardProps & PropsWithChildren> = ({ title, children }) => (
<div className="card"><h2>{title}</h2>{children}</div>
)常用 React 类型速查
| 类型 | 用途 |
|---|---|
ReactNode |
任何可渲染内容(JSX、string、number、null…),最宽泛 |
JSX.Element |
JSX 表达式的直接返回类型 |
CSSProperties |
内联 style 对象 |
ComponentProps<typeof Comp> |
提取组件的 Props 类型 |
ComponentPropsWithoutRef<"button"> |
原生元素的 Props(不含 ref) |
MutableRefObject<T> |
useRef 创建的可变 ref 对象 |
RefObject<T> |
createRef 创建的不可变 ref 对象 |
💡 最佳实践:用
ComponentProps<typeof X>提取已有组件的 props,避免手写重复类型。例如type ButtonProps = ComponentProps<typeof Button>。
Hooks 类型
// useState — 大多数情况自动推断
const [count, setCount] = useState(0) // number
const [user, setUser] = useState<User | null>(null) // 初始为 null 时需显式标注
const [status, setStatus] = useState<"idle" | "loading">("idle") // 字面量联合
// useRef — 必须显式标注
const inputRef = useRef<HTMLInputElement>(null) // ref 绑定 DOM
const timerRef = useRef<ReturnType<typeof setTimeout>>() // 存储 timer id
// useReducer — 与可辨识联合是天作之合
type Action =
| { type: "inc" }
| { type: "dec" }
| { type: "reset"; payload: number }
function reducer(state: number, action: Action): number {
switch (action.type) {
case "inc": return state + 1
case "dec": return state - 1
case "reset": return action.payload
}
}
const [state, dispatch] = useReducer(reducer, 0)
dispatch({ type: "inc" }) // ✅ OK
// dispatch({ type: "unknown" }) // ❌ 编译错误
💡 最佳实践:
useReducer+ 可辨识联合 = 最安全的状态管理组合。TypeScript 能对 Action 进行穷尽性检查,添加新 action 类型时编译器会提醒你更新 reducer。
事件处理器类型
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) =>
console.log(e.currentTarget.textContent)
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) =>
console.log(e.target.value)
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault()
// ...
}常用事件类型:MouseEvent<T> / ChangeEvent<T> / FormEvent<T> / KeyboardEvent<T> / FocusEvent<T> / DragEvent<T>。
🚨 陷阱:
e.target在ChangeEvent中已收窄为HTMLInputElement,但在MouseEvent中仍是泛型EventTarget。需要精确的currentTarget时使用MouseEvent<HTMLButtonElement>。
泛型组件
interface ListProps<T> {
items: T[]
renderItem: (item: T, index: number) => React.ReactNode
}
function List<T>({ items, renderItem }: ListProps<T>): JSX.Element {
return <ul>{items.map((item, i) => <li key={i}>{renderItem(item, i)}</li>)}</ul>
}
// T 自动推断为 { name: string }
<List items={[{ name: "A" }, { name: "B" }]} renderItem={u => u.name} />Node.js + TypeScript
Express 路由类型
import { Request, Response, NextFunction } from "express"
// 获取单个资源
app.get("/users/:id", async (req: Request<{ id: string }>, res: Response) => {
const userId = req.params.id // string
const user = await findUser(userId)
if (!user) return res.status(404).json({ error: "Not found" })
res.json(user)
})
// 使用中间件扩展 Request
interface AuthRequest extends Request {
user?: { id: string; role: string }
}
app.get("/me", authMiddleware, (req: AuthRequest, res: Response) => {
res.json({ id: req.user!.id }) // req.user is guaranteed by middleware
})常用 Express 泛型参数:
Request<Params, ResBody, ReqBody, ReqQuery>
// ^^^^^^ ^^^^^^^ ^^^^^^^ ^^^^^^^^
// req.params 自定义响应体 req.body req.query
环境变量类型
// env.d.ts
declare namespace NodeJS {
interface ProcessEnv {
NODE_ENV: "development" | "production" | "test"
PORT: string
DATABASE_URL: string
}
}
// 使用时获得完整的类型检查
const port = parseInt(process.env.PORT, 10) // PORT is string
日常模式
API 响应泛型封装
几乎所有后端 API 都有统一的响应格式,用泛型一次性定义:
interface ApiResponse<T> {
code: number
message: string
data: T
}
interface ListResponse<T> {
items: T[]
total: number
page: number
}
async function get<T>(url: string): Promise<ApiResponse<T>> {
const res = await fetch(url)
return res.json()
}
// 调用时获得完整类型
const userRes = await get<User>("/api/user/1")
const listRes = await get<User[]>("/api/users")
// userRes.data 类型为 User
// listRes.data 类型为 User[]
Result 类型模式(Rust 风格)
用可辨识联合替代 try-catch 中丢失的类型信息:
type Result<T, E = Error> =
| { success: true; value: T }
| { success: false; error: E }
function safeParse<T>(input: string): Result<T> {
try {
return { success: true, value: JSON.parse(input) as T }
} catch (e) {
return { success: false, error: e as Error }
}
}
const result = safeParse<User>('{"name":"Willow"}')
if (result.success) {
result.value.name // ✅ 类型安全访问
} else {
result.error.message
}💡 最佳实践:Result 模式让错误处理「显式化」——调用方必须处理两种分支,避免了遗漏 null 检查导致的运行时错误。
异步错误处理
// ❌ 错误:catch (e: any) 失去类型信息
try {
await riskyOperation()
} catch (e: any) { // any 会让后续代码失去类型检查
console.log(e.message)
}
// ✅ 正确:catch (e: unknown) + 类型守卫
try {
await riskyOperation()
} catch (e: unknown) {
if (e instanceof Error) {
console.error(e.message)
} else {
console.error("Unknown error:", String(e))
}
}🚨 陷阱:TS 4.0 起
catch子句的变量类型默认是unknown(之前是any)。这要求你在使用前进行类型收窄——这是一个安全特性,不要关闭。
类型守卫在数组过滤中的应用
const items: (string | null | undefined)[] = ["a", null, "b", undefined, "c"]
// 使用类型谓词让 filter 后的数组类型收窄
const validItems: string[] = items.filter(
(item): item is string => item != null
)
// validItems = ["a", "b", "c"],类型为 string[](排除 null 和 undefined)
从常量推导类型
const COLORS = { red: "#ff0000", green: "#00ff00", blue: "#0000ff" } as const
type Color = keyof typeof COLORS // "red" | "green" | "blue"
type ColorHex = typeof COLORS[Color] // "#ff0000" | "#00ff00" | "#0000ff"
function paint(color: Color): void { /* ... */ }
paint("red") // ✅ OK
// paint("cyan") // ❌ Error
💡 最佳实践:
as const+keyof typeof是从运行时数据自动推导类型的最简方式,无需手动维护两份定义。
泛型工厂函数
function createCache<T>() {
const store = new Map<string, T>()
return {
get: (key: string): T | undefined => store.get(key),
set: (key: string, value: T): void => { store.set(key, value) },
}
}
const userCache = createCache<User>()
userCache.set("u1", { name: "Willow", age: 18 })
const u = userCache.get("u1") // User | undefined
常见陷阱汇总
| 陷阱 | 严重度 | 说明 |
|---|---|---|
| React.FC 隐式 children | 🟡 中 | 不需要 children 的组件应直接标注返回值类型 |
catch (e: any) |
🔴 高 | TS 4.0+ 的 catch 变量是 unknown,需类型守卫 |
useRef 未指定类型 |
🟡 中 | useRef(null) 会被推断为 MutableRefObject<null> |
process.env 未经扩展 |
🟡 中 | 用 declare namespace NodeJS 为环境变量添加类型 |
忘记 as const |
🟢 低 | 常量对象的属性会被推断为宽泛类型而非字面量 |
| filter 后类型未收窄 | 🟡 中 | 使用类型谓词 (item): item is T => ... 让数组类型收窄 |