@[toc]
如果你在 RN 项目里用了 TypeScript,但还是经常遇到下面这些情况,那你大概率不是一个人:
- 业务一多,代码开始到处飞,找逻辑像在翻垃圾堆
- hooks 套 hooks,useEffect 里再调 useEffect,自己都不敢改
- 类型文件一堆,但和真实实现经常对不上
- 新人一来就问:"这个状态到底在哪儿改的?"
这篇文章不是教你"TypeScript 怎么写",而是教你:
在 RN 项目里,TypeScript 怎么和架构一起用,才能不失控。
一、为什么 RN + TS 项目特别容易"写乱"?
先说一个结论:
项目乱,不是因为 TypeScript,而是"职责边界不清 + 状态乱流"。
1. 业务增长,逻辑开始横向扩散
最常见的场景:
- 页面里写请求
- hook 里也写请求
- redux 里再写一套
- utils 里还有一份"备用逻辑"
结果是:
一个需求改 4 个地方,还不一定改全。
2. Hooks 层级复杂,本来是解耦,最后变成"黑盒"
你可能写过类似这样的代码:
ts
function usePageLogic() {
const data = useFetch()
const result = useProcess(data)
useEffect(() => {
doSomething(result)
}, [result])
}
问题不是 hooks 多,而是:
- hooks 里混了业务规则
- hooks 之间有隐式依赖
- 调试时根本不知道是哪一层出问题
3. 类型与实现分离,最后谁都不信类型
常见症状:
- interface 写得很全
- 实际接口返回偷偷多字段 / 少字段
- any 越用越多
- 最后 TS 只剩"自动补全工具"的作用
二、一个能长期维护的 RN + TS 项目结构
先给你一个推荐结构总览,这是我在中大型 RN 项目里反复验证过的。
txt
src/
├── domain/ // 业务模型 & 业务规则
├── service/ // API / SDK / 数据来源
├── hooks/ // 可复用 hooks
├── store/ // 全局状态
├── ui/ // 纯 UI 组件
├── pages/ // 页面(只组装,不写重逻辑)
├── utils/
└── types/
接下来我们逐层拆。
三、domain 层:业务规则的"唯一入口"
domain 是整个项目的核心。
你应该在 domain 里放什么?
- 业务实体(User、Order)
- 业务规则(状态转换、校验)
- 与 UI、网络无关的逻辑
示例:用户领域模型
ts
// domain/user.ts
export interface User {
id: string
name: string
role: 'admin' | 'user'
}
export function isAdmin(user: User) {
return user.role === 'admin'
}
关键点:
- domain 不 import RN、API、store
- domain 只关心"业务正确性"
四、service 层:所有"数据来源"的统一出口
service 只做一件事:
把外部世界的数据,变成 domain 能用的数据。
示例:请求用户接口
ts
// service/userService.ts
import { User } from '@/domain/user'
export async function fetchUser(): Promise<User> {
const res = await fetch('/user')
return res.json()
}
不要在页面里直接 fetch。
五、hooks 层:封装"状态 + 行为",但不写业务规则
一个非常重要的原则:
hooks = orchestration,不是 business logic。
示例:useUser
ts
// hooks/useUser.ts
import { useEffect, useState } from 'react'
import { fetchUser } from '@/service/userService'
import { User } from '@/domain/user'
export function useUser() {
const [user, setUser] = useState<User | null>(null)
useEffect(() => {
fetchUser().then(setUser)
}, [])
return user
}
如果你发现 hook 里开始写 if (role === 'admin')
那说明:逻辑该下沉到 domain 了。
六、UI 层:彻底"无脑"的组件
UI 组件只负责:
- 接 props
- 渲染
- 触发回调
示例:用户卡片
tsx
type Props = {
name: string
onPress: () => void
}
export function UserCard({ name, onPress }: Props) {
return (
<Pressable onPress={onPress}>
<Text>{name}</Text>
</Pressable>
)
}
UI 层不 import hooks、不 import service。
七、页面(Page):只做"组装"
页面是"最脏但最轻"的一层。
tsx
export function UserPage() {
const user = useUser()
if (!user) return null
return (
<UserCard
name={user.name}
onPress={() => {}}
/>
)
}
页面:
- 可以用 hook
- 可以用 store
- 但不写核心逻辑
八、全局状态方案怎么选?
这是 RN 项目里非常容易踩坑的一点。
Redux Toolkit
适合:
- 状态多
- 多页面共享
- 对可预测性要求高
优点:
- 规范
- DevTools 强
缺点:
- 样板代码略多
Zustand
适合:
- 中小项目
- 状态简单
- 希望写得快
ts
const useStore = create(set => ({
count: 0,
inc: () => set(state => ({ count: state.count + 1 }))
}))
Recoil
适合:
- 状态依赖复杂
- 类似图结构
但心里要有数:生态和长期维护要评估。
九、TypeScript 在 RN 项目的最佳实践
1. 类型从 domain 开始
不要一上来就写:
ts
type Props = any
类型应该从业务模型流出。
2. 禁止"到处定义重复类型"
统一出口:
ts
export type { User } from '@/domain/user'
3. 不要为了 TS 而 TS
有些地方允许不完美:
ts
const ref = useRef<any>(null)
关键在于:核心业务链路一定要有类型。
十、常见反模式(请尽量避免)
1. 滥用 useEffect 做状态管理
ts
useEffect(() => {
setA(calcB(b))
}, [b])
这种写多了,状态会完全失控。
2. 一个 hook 管一切
ts
usePageLogic()
内部 500 行代码,没人敢碰。
3. domain 被 UI 污染
ts
import { Alert } from 'react-native'
一旦 domain import RN,架构就开始塌。
十一、真实项目里的收益
在真实 RN 项目中,用这套结构后:
- 新需求平均开发时间 ↓ 30%+
- Bug 定位明显更快
- 新人 1~2 天能独立改需求
- TS 不再是"摆设"
最后的总结
如果你只记住一句话:
RN + TypeScript 的核心不是"写类型",而是"让结构帮你兜住复杂度"。
TypeScript 是放大器:
- 架构好 → TS 越用越爽
- 架构乱 → TS 只会更痛苦