强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

TypeScript 开发指南 / 09 - 类型收窄

类型收窄

类型收窄(Type Narrowing)是 TypeScript 中通过条件判断将宽泛类型收窄为更具体类型的过程。

控制流分析(Control Flow Analysis)

TypeScript 通过分析代码的控制流来自动收窄类型:

function example(value: string | number | boolean): void {
  // 此处 value: string | number | boolean

  if (typeof value === "string") {
    // 此处 value: string
    console.log(value.toUpperCase());
  } else if (typeof value === "number") {
    // 此处 value: number
    console.log(value.toFixed(2));
  } else {
    // 此处 value: boolean
    console.log(value ? "yes" : "no");
  }
}

typeof 类型守卫

function padLeft(value: string | number, padding: string | number): string {
  if (typeof padding === "number") {
    // padding: number
    return " ".repeat(padding) + value;
  }
  // padding: string
  return padding + value;
}

// typeof 的所有有效返回值
type TypeofResult =
  | "string"
  | "number"
  | "bigint"
  | "boolean"
  | "symbol"
  | "undefined"
  | "object"
  | "function";

function process(value: unknown): void {
  if (typeof value === "string") { /* string */ }
  if (typeof value === "number") { /* number */ }
  if (typeof value === "boolean") { /* boolean */ }
  if (typeof value === "undefined") { /* undefined */ }
  if (typeof value === "function") { /* Function */ }
  if (typeof value === "object") { /* object | null */ }
}

注意typeof null 返回 "object",所以检查 null 需要额外判断。

真值收窄(Truthiness Narrowing)

function printAll(strs: string | string[] | null): void {
  // 真值检查排除 null、undefined、""、0、NaN
  if (strs && typeof strs === "object") {
    // strs: string[](排除了 null)
    for (const s of strs) {
      console.log(s);
    }
  } else if (typeof strs === "string") {
    // strs: string
    console.log(strs);
  }
}

// ⚠️ 注意:空字符串是 falsy 值
function test(value: string | null): void {
  if (value) {
    // value: string(但不包括空字符串 "")
    console.log(value.toUpperCase());
  }
  // 如果需要包含空字符串,使用显式检查
  if (value !== null) {
    // value: string(包含空字符串)
  }
}

相等性收窄(Equality Narrowing)

function example(x: string | number, y: string | boolean): void {
  if (x === y) {
    // x 和 y 都是 string(公共类型)
    console.log(x.toUpperCase()); // ✅ string
    console.log(y.toUpperCase()); // ✅ string
  }
}

// 使用 switch
function switchExample(value: string | number | boolean): void {
  switch (typeof value) {
    case "string":
      console.log(value.toUpperCase()); // string
      break;
    case "number":
      console.log(value.toFixed(2));    // number
      break;
    case "boolean":
      console.log(value ? "yes" : "no"); // boolean
      break;
  }
}

// null 检查
function nullCheck(value: string | null | undefined): void {
  if (value != null) {
    // value: string(排除 null 和 undefined)
    console.log(value.toUpperCase());
  }

  if (value !== null && value !== undefined) {
    // 同上,更明确
  }
}

instanceof 类型守卫

function formatDate(value: string | Date): string {
  if (value instanceof Date) {
    // value: Date
    return value.toISOString();
  }
  // value: string
  return new Date(value).toISOString();
}

// 自定义类
class ApiError {
  constructor(public status: number, public message: string) {}
}

class NetworkError {
  constructor(public code: string) {}
}

function handleError(error: ApiError | NetworkError | Error): string {
  if (error instanceof ApiError) {
    return `API 错误: ${error.status} - ${error.message}`;
  }
  if (error instanceof NetworkError) {
    return `网络错误: ${error.code}`;
  }
  return `未知错误: ${error.message}`;
}

in 操作符收窄

interface Bird {
  fly(): void;
  layEggs(): void;
}

interface Fish {
  swim(): void;
  layEggs(): void;
}

function move(animal: Bird | Fish): void {
  if ("fly" in animal) {
    // animal: Bird
    animal.fly();
  } else {
    // animal: Fish
    animal.swim();
  }
}

// 可选属性
interface Circle { kind: "circle"; radius: number; }
interface Square { kind: "square"; sideLength: number; }

function area(shape: Circle | Square): number {
  if ("radius" in shape) {
    // shape: Circle
    return Math.PI * shape.radius ** 2;
  }
  // shape: Square
  return shape.sideLength ** 2;
}

可辨识联合收窄

// 每个成员都有共同的 discriminant 属性
type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; sideLength: number }
  | { kind: "rectangle"; width: number; height: number }
  | { kind: "triangle"; base: number; height: number };

// 使用 switch 收窄
function getArea(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.sideLength ** 2;
    case "rectangle":
      return shape.width * shape.height;
    case "triangle":
      return 0.5 * shape.base * shape.height;
  }
}

// 使用 if 收窄
function describe(shape: Shape): string {
  if (shape.kind === "circle") {
    return `圆,半径 ${shape.radius}`;
  }
  if (shape.kind === "square") {
    return `正方形,边长 ${shape.sideLength}`;
  }
  // ...
}

自定义类型守卫(Type Predicates)

// 语法:parameterName is Type
function isString(value: unknown): value is string {
  return typeof value === "string";
}

function isNumber(value: unknown): value is number {
  return typeof value === "number" && !isNaN(value);
}

function isArray<T>(value: T | T[]): value is T[] {
  return Array.isArray(value);
}

// 使用
function process(value: unknown): void {
  if (isString(value)) {
    console.log(value.toUpperCase()); // value: string
  } else if (isNumber(value)) {
    console.log(value.toFixed(2));    // value: number
  }
}

复杂类型守卫

interface User {
  id: number;
  name: string;
  email?: string;
}

interface Admin extends User {
  role: "admin";
  permissions: string[];
}

function isAdmin(user: User): user is Admin {
  return "role" in user && (user as Admin).role === "admin";
}

function handleUser(user: User | Admin): void {
  if (isAdmin(user)) {
    // user: Admin
    console.log(user.permissions);
  } else {
    // user: User
    console.log(user.name);
  }
}

断言函数(Assertion Functions)

// 断言函数:如果不满足条件则抛出错误
function assertIsDefined<T>(
  value: T,
  name: string
): asserts value is NonNullable<T> {
  if (value === null || value === undefined) {
    throw new Error(`Expected ${name} to be defined, got ${value}`);
  }
}

function processValue(value: string | null | undefined): void {
  assertIsDefined(value, "value");
  // 这里 value: string
  console.log(value.toUpperCase());
}

// 自定义断言
function assertIsNumber(
  value: unknown,
  name: string
): asserts value is number {
  if (typeof value !== "number" || isNaN(value)) {
    throw new Error(`${name} must be a valid number`);
  }
}

function calculate(input: unknown): number {
  assertIsNumber(input, "input");
  // input: number
  return input * 2;
}

收窄的链式传播

interface ApiResponse {
  data: {
    user: {
      name: string;
      address?: {
        city: string;
        zip?: string;
      };
    };
  };
}

function getCity(response: ApiResponse): string {
  // 需要逐层收窄
  if (!response.data?.user?.address?.city) {
    return "未知城市";
  }
  return response.data.user.address.city;
}

// 使用可选链 + 空值合并
function getCityV2(response: ApiResponse): string {
  return response.data.user.address?.city ?? "未知城市";
}

业务场景:API 错误处理

// 使用可辨识联合处理 API 响应
type ApiResult<T> =
  | { success: true; data: T; timestamp: number }
  | { success: false; error: ApiError; timestamp: number };

interface ApiError {
  code: number;
  message: string;
  details?: Record<string, string[]>;
}

interface User {
  id: number;
  name: string;
}

async function fetchUser(id: number): Promise<ApiResult<User>> {
  try {
    const response = await fetch(`/api/users/${id}`);
    if (!response.ok) {
      const error: ApiError = await response.json();
      return { success: false, error, timestamp: Date.now() };
    }
    const data = await response.json();
    return { success: true, data, timestamp: Date.now() };
  } catch (e) {
    return {
      success: false,
      error: { code: 0, message: "网络错误" },
      timestamp: Date.now()
    };
  }
}

// 使用
const result = await fetchUser(1);

if (result.success) {
  // result.data: User
  console.log(`用户名: ${result.data.name}`);
} else {
  // result.error: ApiError
  console.error(`错误 ${result.error.code}: ${result.error.message}`);
}

常见陷阱

1. 闭包中的类型收窄丢失

function example(value: string | null): void {
  if (value !== null) {
    // value: string ✅

    setTimeout(() => {
      // ❌ value 可能为 null(闭包捕获了变量引用)
      // console.log(value.toUpperCase());

      // ✅ 解决方案:在收窄后赋值给局部常量
      const safeValue = value;
      console.log(safeValue.toUpperCase());
    }, 1000);
  }
}

2. 数组中的类型收窄

const mixed: (string | number)[] = [1, "two", 3, "four"];

// ❌ 不能直接用 typeof 过滤后获得类型安全的数组
const strings = mixed.filter(item => typeof item === "string");
// 类型仍然是 (string | number)[]

// ✅ 使用类型守卫
function isString(value: unknown): value is string {
  return typeof value === "string";
}
const safeStrings = mixed.filter(isString);
// 类型是 string[]

3. 类型断言 vs 类型收窄

function bad(value: unknown): string {
  // ❌ 类型断言:不安全,绕过检查
  return (value as string).toUpperCase();
}

function good(value: unknown): string {
  // ✅ 类型收窄:安全,编译器验证
  if (typeof value === "string") {
    return value.toUpperCase();
  }
  throw new Error("Expected string");
}

注意事项

  1. 优先使用类型收窄而非类型断言——更安全
  2. 可辨识联合是处理复杂联合类型的最佳模式
  3. 自定义类型守卫适用于无法通过内置方式收窄的场景
  4. 注意闭包中类型收窄可能丢失——必要时使用局部常量
  5. 空值检查是类型收窄的常见场景,使用 != null 同时检查 nullundefined

扩展阅读