TypeScript 与前端框架:React、Node.js
TypeScript 与现代前端框架是天生一对。这一期,我们学习 TypeScript 在 React 和 Node.js 中的应用。
React + TypeScript
创建项目
# Vite(推荐)
npm create vite@latest my-app -- --template react-ts
# Create React App
npx create-react-app my-app --template typescript
# Next.js
npx create-next-app@latest my-app --typescript
组件类型
函数组件
// 简单组件
const Button: React.FC = () => {
return <button>Click me</button>
}
// 带 Props 的组件
interface ButtonProps {
text: string
onClick: () => void
disabled?: boolean
}
const Button: React.FC = ({
text,
onClick,
disabled = false
}) => {
return (
<button disabled="{disabled}">
{text}
</button>
)
}
内联 Props
type UserCardProps = {
name: string
email: string
avatar?: string
}
function UserCard({ name, email, avatar }: UserCardProps) {
return (
<div>
{avatar && <img src="{avatar}" alt="{name}" />}
<h3>{name}</h3>
<p>{email}</p>
</div>
)
}
Hooks 类型
useState
// 类型推断
const [count, setCount] = useState(0)
// 显式类型
const [user, setUser] = useState(null)
// 复杂类型
const [items, setItems] = useState([])
useEffect
useEffect(() => {
// 副作用逻辑
return () => {
// 清理函数
}
}, [dependency])
useRef
// DOM 引用
const inputRef = useRef(null)
// 任意值引用
const timerRef = useRef(null)
useContext
interface ThemeContextType {
theme: 'light' | 'dark'
toggleTheme: () => void
}
const ThemeContext = createContext(null)
function useTheme() {
const context = useContext(ThemeContext)
if (!context) {
throw new Error('useTheme must be used within ThemeProvider')
}
return context
}
useReducer
type State = {
count: number
}
type Action =
| { type: 'increment' }
| { type: 'decrement' }
| { type: 'reset'; payload: number }
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'increment':
return { count: state.count + 1 }
case 'decrement':
return { count: state.count - 1 }
case 'reset':
return { count: action.payload }
}
}
const [state, dispatch] = useReducer(reducer, { count: 0 })
事件类型
function Form() {
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault()
}
const handleChange = (e: React.ChangeEvent) => {
console.log(e.target.value)
}
const handleClick = (e: React.MouseEvent) => {
console.log('clicked')
}
return (
<button>Submit</button>
)
}
泛型组件
interface ListProps {
items: T[]
renderItem: (item: T) => React.ReactNode
}
function List({ items, renderItem }: ListProps) {
return (
<ul>
{items.map((item, index) => (
<li>{renderItem(item)}</li>
))}
</ul>
)
}
// 使用
<span>{user.name}</span>}
/>
高阶组件
interface WithLoadingProps {
loading?: boolean
}
function withLoading<P>(
Component: React.ComponentType<P>
): React.FC<P> {
return ({ loading, ...props }) => {
if (loading) {
return <div>Loading...</div>
}
return
}
}
Node.js + TypeScript
项目设置
mkdir my-project
cd my-project
npm init -y
npm install typescript ts-node @types/node --save-dev
npx tsc --init
tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
Express 示例
npm install express
npm install @types/express --save-dev
import express, { Request, Response, NextFunction } from 'express'
const app = express()
app.use(express.json())
// 类型定义
interface User {
id: number
name: string
email: string
}
// 内存数据库
const users: User[] = []
// 路由
app.get('/users', (req: Request, res: Response) => {
res.json(users)
})
app.get('/users/:id', (req: Request, res: Response) => {
const user = users.find(u => u.id === parseInt(req.params.id))
if (!user) {
res.status(404).send('User not found')
return
}
res.json(user)
})
app.post('/users', (req: Request<{}, User, Omit>, res: Response) => {
const newUser: User = {
id: users.length + 1,
...req.body
}
users.push(newUser)
res.status(201).json(newUser)
})
// 错误处理
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
console.error(err.stack)
res.status(500).json({ error: err.message })
})
app.listen(3000, () => {
console.log('Server running on http://localhost:3000')
})
中间件类型
interface AuthRequest extends Request {
user?: {
id: number
role: string
}
}
function authMiddleware(
req: AuthRequest,
res: Response,
next: NextFunction
) {
const token = req.headers.authorization
if (!token) {
res.status(401).json({ error: 'Unauthorized' })
return
}
// 验证 token...
req.user = { id: 1, role: 'admin' }
next()
}
app.get('/protected', authMiddleware, (req: AuthRequest, res: Response) => {
res.json({ user: req.user })
})
数据库集成
// Prisma 示例
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
async function getUsers() {
const users = await prisma.user.findMany({
include: { posts: true }
})
return users
}
async function createUser(data: { name: string; email: string }) {
const user = await prisma.user.create({
data
})
return user
}
工具函数
API 客户端
interface HttpClient { get(url: string): Promise post(url: string, data: unknown): Promise put(url: string, data: unknown): Promise delete(url: string): Promise } class FetchHttpClient implements HttpClient { private baseUrl: string constructor(baseUrl: string) { this.baseUrl = baseUrl } async get(url: string): Promise { const response = await fetch(<code>${this.baseUrl}${url}) if (!response.ok) { throw new Error(HTTP error! status: ${response.status}) } return response.json() } async post(url: string, data: unknown): Promise { const response = await fetch(${this.baseUrl}${url}, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }) return response.json() } // ... 其他方法 }
状态管理
// 简单的状态管理器
type Listener = (state: T) => void
class Store {
private state: T
private listeners: Listener[] = []
constructor(initialState: T) {
this.state = initialState
}
getState(): T {
return this.state
}
setState(newState: Partial) {
this.state = { ...this.state, ...newState }
this.listeners.forEach(listener => listener(this.state))
}
subscribe(listener: Listener) {
this.listeners.push(listener)
return () => {
this.listeners = this.listeners.filter(l => l !== listener)
}
}
}
// 使用
interface AppState {
user: User | null
loading: boolean
}
const store = new Store({
user: null,
loading: false
})
测试
Jest 配置
npm install --save-dev jest ts-jest @types/jest
npx ts-jest config:init
单元测试
// math.ts
export function add(a: number, b: number): number {
return a + b
}
// math.test.ts
import { add } from './math'
describe('add', () => {
it('should add two numbers', () => {
expect(add(1, 2)).toBe(3)
})
})
React 组件测试
// Button.tsx
interface ButtonProps {
text: string
onClick: () => void
}
export const Button: React.FC = ({ text, onClick }) => {
return <button>{text}</button>
}
// Button.test.tsx
import { render, screen, fireEvent } from '@testing-library/react'
import { Button } from './Button'
describe('Button', () => {
it('should render with text', () => {
render(<Button> {}} />)
expect(screen.getByText('Click me')).toBeInTheDocument()
})
it('should call onClick when clicked', () => {
const handleClick = jest.fn()
render(<Button />)
fireEvent.click(screen.getByText('Click me'))
expect(handleClick).toHaveBeenCalledTimes(1)
})
})
实战技巧
类型安全的 API 路由
// 路由定义
type Route = {
path: string
method: 'GET' | 'POST' | 'PUT' | 'DELETE'
handler: (req: Request, res: Response) => void | Promise
}
const routes: Route[] = [
{
path: '/users',
method: 'GET',
handler: getUsers
},
{
path: '/users',
method: 'POST',
handler: createUser
}
]
// 自动注册路由
routes.forEach(route => {
app[route.method.toLowerCase()](route.path, route.handler)
})
类型安全的环境变量
// env.ts
import { z } from 'zod'
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']),
PORT: z.string().transform(Number),
DATABASE_URL: z.string(),
JWT_SECRET: z.string()
})
export const env = envSchema.parse(process.env)
总结
TypeScript 与框架结合要点:
- React:组件类型、Hooks 类型、事件类型
- Node.js:Express 类型、中间件、数据库
- 测试:Jest + ts-jest
- 最佳实践:类型安全的 API、环境变量
下一期,我们学习 Vue3 + TypeScript 的完整集成指南。
框架文档:
Views: 0
