从“可能崩溃”到“必须安全”的类型革命
在传统开发中,我们经常编写“说谎”的代码:
// ❌ 我们在说谎!
interface User {
id: number;
name: string;
}
function findUser(id: number): User {
// 但实际上:可能找不到用户!
return database.findUser(id); // 可能返回 undefined
}
const user = findUser(123);
console.log(user.name); // 💥 运行时崩溃!
问题根源:类型系统与运行时脱节
我们面临的核心问题是:类型声明无法反映真实世界的不确定性。
// 这些类型都在说谎:
interface ApiResponse {
data: User; // 但请求可能失败!
error?: string; // 模糊的optional无法表达互斥关系
}
function parseInput(input: string): number {
// 但输入可能无法解析!
return parseInt(input); // 可能返回 NaN
}
这种脱节导致:
- 运行时崩溃:类型说“一定有”,运行时说“其实没有”
- 复杂防御代码:到处都是
if (x && x.y && x.y.z) - 业务逻辑模糊:状态关系靠注释,而不是代码表达
解决方案:联合类型 + 类型收窄
1. 基础联合:表达“或”的关系
// ✅ 说出真相:这个值可能是字符串或数字
type ID = string | number;
// ✅ 明确表达:函数可能成功或失败
function findUser(id: number): User | undefined {
return database.findUser(id);
}
const user = findUser(123);
// ❌ TypeScript报错:Object is possibly 'undefined'
console.log(user.name);
// ✅ 必须显式处理
if (user) {
console.log(user.name); // 这里user被收窄为User类型
} else {
console.log('用户不存在');
}
2. 字面量联合:有限的选项集合
// 明确的选项,杜绝无效值
type ButtonSize = 'small' | 'medium' | 'large';
type Theme = 'light' | 'dark' | 'auto';
function setTheme(theme: Theme) {
// 不用检查theme是否是有效值,TypeScript已经保证!
applyTheme(theme);
}
setTheme('light'); // ✅
setTheme('unknown'); // ❌ TypeScript编译错误
3. 可辨识联合:让不可能的状态变得不可能
这是联合类型的杀手级特性:
// ❌ 传统方式:矛盾的状态可能共存
interface FormState {
isLoading: boolean;
data?: User[];
error?: string;
// 可能同时存在:isLoading=true, error="xxx", data=[...]
}
// ✅ 联合类型:排除矛盾状态
type FormState =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: User[] }
| { status: 'error'; message: string };
// 现在不可能同时处于loading和error状态!
实战:重构复杂业务逻辑
Before:脆弱的条件判断
function handleApiResponse(response) {
// 需要记住所有可能的字段组合
if (response.success && response.data) {
return response.data.map(item => item.name);
} else if (response.error) {
if (response.error.code === 404) {
return '未找到';
}
// 容易遗漏其他错误码...
} else if (response.loading) {
return '加载中...';
}
// 可能忘记处理其他情况...
}
After:完整的状态机
type ApiResponse =
| { type: 'success'; data: User[] }
| { type: 'error'; message: string; code: number }
| { type: 'loading'; progress: number }
| { type: 'idle' };
function handleApiResponse(response: ApiResponse) {
switch (response.type) {
case 'success':
return response.data.map(user => user.name);
case 'error':
return `错误 ${response.code}: ${response.message}`;
case 'loading':
return `加载中... ${response.progress}%`;
case 'idle':
return '准备就绪';
default:
// 如果未来添加新类型,这里会报错!
const exhaustiveCheck: never = response;
return exhaustiveCheck;
}
}
类型收窄:安全访问的四种方式
1. typeof 和 instanceof
function process(value: string | number) {
if (typeof value === 'string') {
return value.toUpperCase(); // value被收窄为string
} else {
return value.toFixed(2); // value被收窄为number
}
}
2. in 操作符
interface Dog { bark(): void; }
interface Cat { meow(): void; }
function handlePet(pet: Dog | Cat) {
if ('bark' in pet) {
pet.bark(); // pet被收窄为Dog
} else {
pet.meow(); // pet被收窄为Cat
}
}
3. 自定义类型守卫
function isStringArray(value: unknown): value is string[] {
return Array.isArray(value) && value.every(item => typeof item === 'string');
}
function processData(data: unknown) {
if (isStringArray(data)) {
// data被收窄为string[]
return data.map(s => s.trim());
}
}
React实战:表单状态管理
type FormState =
| { type: 'idle' }
| { type: 'submitting'; data: FormData }
| { type: 'success'; result: Order }
| { type: 'error'; message: string; field?: string };
const ContactForm: React.FC = () => {
const [state, setState] = useState<FormState>({ type: 'idle' });
const handleSubmit = async (data: FormData) => {
setState({ type: 'submitting', data });
try {
const result = await api.submit(data);
setState({ type: 'success', result });
} catch (error) {
setState({ type: 'error', message: error.message });
}
};
return (
<form>
{/* 编译器保证我们处理了所有状态 */}
{state.type === 'submitting' && <Spinner />}
{state.type === 'success' && <Confirmation order={state.result} />}
{state.type === 'error' && <ErrorMessage message={state.message} />}
<button
disabled={state.type === 'submitting'}
onClick={() => handleSubmit(formData)}
>
提交
</button>
</form>
);
};
为什么这是编程范式的升级?
传统开发:事后检查
写代码 → 运行测试 → 发现边缘情况 → 打补丁 → 引入新bug
联合类型:事前预防
定义完整类型 → 编译器检查所有情况 → 编写安全代码 → 运行时稳定
核心价值对比
| 维度 | 传统方式 | 联合类型 |
|---|---|---|
| 安全性 | 运行时发现错误 | 编译时防止错误 |
| 可维护性 | 靠注释和记忆 | 类型即文档 |
| 重构信心 | 担心破坏隐藏逻辑 | 编译器保证完整性 |
| 业务表达 | 逻辑藏在代码里 | 类型表达业务规则 |
开始行动
第一步:识别“说谎”的代码
// 找到这些模式:
interface X { data: Y } // 但data可能不存在
function f(): Z // 但可能返回null/undefined
第二步:用联合类型说出真相
// 改为:
type X = { data: Y } | { error: string }
function f(): Z | null
第三步:享受编译器保驾护航
// TypeScript会成为你的结对编程伙伴:
// - 提醒你处理所有情况
// - 阻止你访问可能不存在的属性
// - 保证重构的安全性
对!你抓住了联合类型的精髓:预先设计的精确性。
联合类型 vs 传统方式的本质区别
传统方式:运行时猜测
function processData(data: any) {
// 编码时:❌ 不知道data具体结构
// 运行时:🤔 猜测并检查
if (data && typeof data === 'object') {
if ('items' in data && Array.isArray(data.items)) {
return data.items.map(item => item.name); // 祈祷item有name属性
}
}
// 可能遗漏很多情况...
}
联合类型:编码时精确设计
// 编码时:✅ 预先定义所有可能性
type ApiData =
| { type: 'user'; name: string; age: number }
| { type: 'product'; name: string; price: number }
| { type: 'error'; message: string };
function processData(data: ApiData) {
// 编码时:✅ 明确知道要处理的所有情况
switch (data.type) {
case 'user':
return `${data.name} (${data.age}岁)`; // ✅ 精确访问
case 'product':
return `${data.name} - ¥${data.price}`; // ✅ 精确访问
case 'error':
return `错误: ${data.message}`; // ✅ 精确访问
}
}
为什么这很重要?
1. 思维方式的转变
从:"我该写什么代码来处理这个数据?" 变为:"这个数据可能是什么样子?"
2. 设计驱动的开发
// 先设计完整的状态机,再写处理逻辑
type OrderFlow =
| { stage: 'draft'; canEdit: true; saved: boolean }
| { stage: 'submitted'; submittedAt: Date; canEdit: false }
| { stage: 'paid'; paidAt: Date; amount: number }
| { stage: 'shipped'; trackingNumber: string }
| { stage: 'completed'; rating?: number };
// 现在写逻辑就是填空,不会遗漏
function handleOrder(order: OrderFlow) {
switch (order.stage) {
case 'draft':
if (order.saved) { /* ... */ } // ✅ 精确知道可用属性
case 'submitted':
console.log(order.submittedAt); // ✅ 精确知道可用属性
// ... 编译器确保处理所有情况
}
}
3. 代码即设计文档
// 看类型就知道业务规则
type PaymentResult =
| { status: 'success'; transactionId: string; amount: number }
| { status: 'failed'; reason: 'insufficient_funds' | 'network_error' | 'timeout' }
| { status: 'pending'; estimatedCompletion: Date };
// 不用看代码实现,就知道:
// - 支付有3种可能结果
// - 失败有3种具体原因
// - 每种结果有哪些相关信息
实际收益:从混乱到条理
Before:维护噩梦
function handleResponse(response) {
// 新同事:这个response到底可能有什么字段?
if (response.success) {
if (response.data) {
if (Array.isArray(response.data)) {
// ... 嵌套越来越深
}
}
} else if (response.error) {
// 可能还有response.errorMessage?response.err?
}
// 6个月后:加新功能时完全不敢改这个函数
}
After:清晰条理
type Response =
| { success: true; data: User[] }
| { success: false; error: string };
function handleResponse(response: Response) {
if (response.success) {
// ✅ 明确知道:这里response.data一定是User[]
return response.data.map(user => user.name);
} else {
// ✅ 明确知道:这里response.error一定是string
return `错误: ${response.error}`;
}
// 新同事一眼看懂所有可能性
// 6个月后:安全地添加新功能
}
这才是真正的"工程化"
联合类型让我们从: "先写代码,再发现边界情况" 升级到: "先定义边界,再写处理逻辑"
这种预先设计的精确性,让代码:
- 🎯 更有条理:逻辑结构清晰可见
- 🔒 更安全:编译时捕获边界情况
- 📚 更易维护:类型即文档
- 🚀 更易扩展:添加新情况时编译器指导修改
这就是现代前端工程化的核心:用类型系统把不确定性消灭在编码阶段。
总结
联合类型不是语法糖,而是编程思维的升级:
- 从模糊到精确:用类型表达真实的业务约束
- 从脆弱到健壮:编译器帮你捕获边缘情况
- 从复杂到简单:排除不可能的状态,简化逻辑
开始使用联合类型,让你的代码不再“说谎”,让编译器成为你最可靠的开发伙伴。
让不可能的状态变得不可能,这就是联合类型的真正威力。