Skip to content
实战集成

实战集成

本章将 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.targetChangeEvent 中已收窄为 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 => ... 让数组类型收窄