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. 区分“内部实现”与“公共边界”

什么时候该写类型,什么时候该依赖推断?

5. 品牌类型 (Branded Types) 与名义类型模拟

TypeScript 是结构化类型系统(Duck Typing)。只要结构像,就认为是同一种类型。 但在某些涉及资金或安全的场景,我们需要区分结构相同但逻辑意义不同的类型(如 UserIdOrderId 都是 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`,让编译器为你工作,而不是与你作对。