TypeScript 类型推断的常见边界
TypeScript 最迷人的地方在于它的类型推断(Type Inference)。 新手往往写满类型注解,而高手则懂得“留白”。 让 TS 编译器自动推导类型,不仅能减少冗余代码,更能让类型随着逻辑自动流动(Type Flow), 实现“写得少,但覆盖得准”。
1. const 断言 (as const):锁定字面量
TS 默认会将 `let s = "hello"` 推断为 `string`,这是一种宽泛的推断。 但在开发配置对象、Redux Action 或常量映射时,我们需要精确的字面量类型。
// 宽泛推断
const config = { mode: 'dark', timeout: 5000 };
// config.mode 是 string
// 锁定字面量
const config = { mode: 'dark', timeout: 5000 } as const;
// config.mode 是 'dark' (readonly)
// config.timeout 是 5000 (readonly)
`as const` 是将对象变为深度只读且类型收窄的最快方式,是构建类型安全的配置系统的利器。
2. satisfies 操作符:类型校验但不丢失推断
TypeScript 4.9 引入的 `satisfies` 是游戏规则改变者。 过去,为了校验一个对象符合 Interface,我们不得不显式标注,但这会导致我们失去具体的推断信息。
type Theme = { [key: string]: string | number };
// ❌ 传统写法:丢失了 'blue' 是 string 的信息,被强转为 string | number
const palette: Theme = { blue: '#00f', opacity: 0.5 };
// palette.blue.toUpperCase(); // 报错,因为 TS 认为它可能是 number
// ✅ satisfies:校验符合 Theme 结构,但保留具体类型推断
const palette = { blue: '#00f', opacity: 0.5 } satisfies Theme;
palette.blue.toUpperCase(); // 正常工作!
3. 泛型推断与 infer 关键字
泛型是类型的函数。通过 `infer` 关键字,我们可以在条件类型中提取(Unwrap)内部的类型。 这在处理复杂的第三方库类型或递归解包 Promise 时非常有用。
// 提取 Promise 内部的返回值类型
type Awaited<T> = T extends Promise<infer U> ? U : T;
type Result = Awaited<Promise<string>>; // string
熟练使用 `extends` 约束和 `infer` 推断,是迈向 TS 类型体操专家的第一步,也是编写通用工具类型的必经之路。
4. 区分“内部实现”与“公共边界”
什么时候该写类型,什么时候该依赖推断?
- 函数内部 (Implementation): 尽可能依赖推断。`let x = 1` 不需要写 `: number`。这让代码更简洁,重构更轻松。
- 公共边界 (Public API): 导出的函数、组件 Props、API 响应接口,必须显式标注。这构成了模块间的“契约”,防止内部变更无意中破坏了外部调用者的类型检查(Breaking Changes)。
5. 品牌类型 (Branded Types) 与名义类型模拟
TypeScript 是结构化类型系统(Duck Typing)。只要结构像,就认为是同一种类型。
但在某些涉及资金或安全的场景,我们需要区分结构相同但逻辑意义不同的类型(如 UserId 和 OrderId 都是 string)。
type Brand<K, T> = K & { __brand: T };
type UserId = Brand<string, 'UserId'>;
type OrderId = Brand<string, 'OrderId'>;
function queryUser(id: UserId) { /* ... */ }
// queryUser(someOrderId); // ❌ 报错:类型不匹配
通过这种"打标签"的方式,我们可以在编译时模拟名义类型系统,大幅提升业务逻辑的安全性。
5.1 实战应用:金额类型安全
// 定义不同币种的金额类型
type Money<Currency extends string> = Brand<number, Currency>;
type USD = Money<'USD'>;
type CNY = Money<'CNY'>;
type EUR = Money<'EUR'>;
// 创建金额的工厂函数
function usd(amount: number): USD {
return amount as USD;
}
function cny(amount: number): CNY {
return amount as CNY;
}
// 计算函数必须使用相同币种
function addMoney<T extends string>(a: Money<T>, b: Money<T>): Money<T> {
return (a + b) as Money<T>;
}
const price1 = usd(100);
const price2 = usd(50);
const total = addMoney(price1, price2); // ✅ 正常
const price3 = cny(300);
// const wrongTotal = addMoney(price1, price3); // ❌ 编译错误:不能混合不同币种
5.2 非空断言的隐藏危险
品牌类型结合非空断言和类型守卫,可以创建更严格的数据流:
type NonEmptyString = Brand<string, 'NonEmpty'>;
function isNonEmpty(str: string): str is NonEmptyString {
return str.length > 0;
}
function sendEmail(to: NonEmptyString, subject: string) {
// 确保邮箱地址非空
console.log(`Sending to: ${to}`);
}
const userEmail = getUserEmail(); // string
if (isNonEmpty(userEmail)) {
sendEmail(userEmail, 'Welcome'); // ✅ 类型收窄为 NonEmptyString
} else {
console.error('Invalid email');
}
小结
TypeScript 的终极境界不是写出满屏的 `any`,也不是写出晦涩难懂的类型体操, 而是利用推断机制,构建出“像 JavaScript 一样灵活,像 Java 一样安全”的代码。 理解推断的边界,善用 `as const` 和 `satisfies`,让编译器为你工作,而不是与你作对。