类型推断与类型守卫

TypeScript 的类型推断让代码更简洁,类型守卫则在运行时确保类型安全。


类型推断

基础推断

TypeScript 会根据上下文自动推断类型:

// 变量推断
let name = "Alice";      // string
let age = 25;            // number
let active = true;       // boolean
 
// 数组推断
let numbers = [1, 2, 3];           // number[]
let mixed = [1, "hello"];          // (string | number)[]
 
// 对象推断
let user = {
  name: "Alice",
  age: 25
}; // { name: string; age: number }

函数返回值推断

// 自动推断返回类型
function add(a: number, b: number) {
  return a + b; // 推断为 number
}
 
function createUser(name: string, age: number) {
  return { name, age }; // 推断为 { name: string; age: number }
}
 
// 箭头函数
const multiply = (a: number, b: number) => a * b; // 推断为 number

上下文类型推断

// 回调函数参数推断
const numbers = [1, 2, 3];
numbers.forEach(num => {
  console.log(num.toFixed(2)); // num 推断为 number
});
 
// 事件处理器
document.addEventListener("click", event => {
  console.log(event.clientX); // event 推断为 MouseEvent
});

最佳通用类型

// TypeScript 选择能容纳所有元素的类型
let arr = [0, 1, null]; // (number | null)[]
 
// 混合类型数组
class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}
 
let pets = [new Dog(), new Cat()]; // (Dog | Cat)[]

类型断言

当你比编译器更了解类型时使用:

as 语法

const input = document.getElementById("input") as HTMLInputElement;
input.value = "hello";
 
// 从 unknown 断言
let value: unknown = "hello";
let length = (value as string).length;

非空断言

function getValue(): string | undefined {
  return "hello";
}
 
// 使用 ! 断言非空
const value = getValue()!;
console.log(value.length); // 确信 value 不为 undefined
 
// DOM 元素非空断言
const element = document.querySelector(".container")!;

const 断言

// 字面量类型
let str = "hello" as const; // "hello"(而非 string)
 
// 只读数组
let arr = [1, 2, 3] as const; // readonly [1, 2, 3]
 
// 只读对象
let obj = { name: "Alice", age: 25 } as const;
// { readonly name: "Alice"; readonly age: 25 }
 
// 用于枚举替代
const Direction = {
  Up: "UP",
  Down: "DOWN",
  Left: "LEFT",
  Right: "RIGHT"
} as const;
 
type DirectionValue = typeof Direction[keyof typeof Direction];
// "UP" | "DOWN" | "LEFT" | "RIGHT"

双重断言

谨慎使用,仅在必要时:

// 当两个类型完全不兼容时
const value = "hello" as unknown as number;

类型守卫

类型守卫用于在运行时缩小类型范围。

typeof 守卫

function process(value: string | number): string {
  if (typeof value === "string") {
    return value.toUpperCase(); // value 是 string
  }
  return value.toFixed(2); // value 是 number
}
 
// 支持的 typeof 检查
// "string" | "number" | "boolean" | "symbol" | "undefined" | "object" | "function" | "bigint"

instanceof 守卫

class Dog {
  bark() { console.log("Woof!"); }
}
 
class Cat {
  meow() { console.log("Meow!"); }
}
 
function speak(animal: Dog | Cat): void {
  if (animal instanceof Dog) {
    animal.bark(); // animal 是 Dog
  } else {
    animal.meow(); // animal 是 Cat
  }
}

in 操作符守卫

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

等值守卫

type Status = "loading" | "success" | "error";
 
function handleStatus(status: Status): void {
  if (status === "loading") {
    console.log("Loading...");
  } else if (status === "success") {
    console.log("Success!");
  } else {
    console.log("Error!"); // status 是 "error"
  }
}

自定义类型守卫

类型谓词

使用 is 关键字创建自定义类型守卫:

interface Cat {
  type: "cat";
  meow(): void;
}
 
interface Dog {
  type: "dog";
  bark(): void;
}
 
// 类型谓词函数
function isCat(animal: Cat | Dog): animal is Cat {
  return animal.type === "cat";
}
 
function isDog(animal: Cat | Dog): animal is Dog {
  return animal.type === "dog";
}
 
function speak(animal: Cat | Dog): void {
  if (isCat(animal)) {
    animal.meow(); // animal 是 Cat
  } else {
    animal.bark(); // animal 是 Dog
  }
}

检查特定属性

interface Admin {
  role: "admin";
  permissions: string[];
}
 
interface User {
  role: "user";
  name: string;
}
 
function isAdmin(person: Admin | User): person is Admin {
  return person.role === "admin";
}
 
function getPermissions(person: Admin | User): string[] {
  if (isAdmin(person)) {
    return person.permissions;
  }
  return []; // User 没有 permissions
}

数组过滤类型守卫

function isNotNull<T>(value: T | null): value is T {
  return value !== null;
}
 
const values: (string | null)[] = ["a", null, "b", null, "c"];
const strings: string[] = values.filter(isNotNull);
// ["a", "b", "c"]
 
// 更通用的版本
function isDefined<T>(value: T | undefined | null): value is T {
  return value !== undefined && value !== null;
}

断言函数

TypeScript 3.7+ 支持断言函数:

function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== "string") {
    throw new Error("Value is not a string");
  }
}
 
function processValue(value: unknown): void {
  assertIsString(value);
  // 在这之后,value 被推断为 string
  console.log(value.toUpperCase());
}

非空断言函数

function assertIsDefined<T>(value: T | undefined | null): asserts value is T {
  if (value === undefined || value === null) {
    throw new Error("Value is not defined");
  }
}
 
function process(value: string | undefined): void {
  assertIsDefined(value);
  // value 现在是 string
  console.log(value.length);
}

可辨识联合与穷尽检查

可辨识联合模式

interface LoadingState {
  status: "loading";
}
 
interface SuccessState {
  status: "success";
  data: string;
}
 
interface ErrorState {
  status: "error";
  error: Error;
}
 
type State = LoadingState | SuccessState | ErrorState;
 
function handleState(state: State): string {
  switch (state.status) {
    case "loading":
      return "Loading...";
    case "success":
      return state.data;
    case "error":
      return state.error.message;
  }
}

穷尽检查

确保处理了所有情况:

function assertNever(value: never): never {
  throw new Error(`Unexpected value: ${value}`);
}
 
function handleState(state: State): string {
  switch (state.status) {
    case "loading":
      return "Loading...";
    case "success":
      return state.data;
    case "error":
      return state.error.message;
    default:
      // 如果添加了新的状态但忘记处理,这里会报编译错误
      return assertNever(state);
  }
}

控制流分析

TypeScript 会跟踪变量在不同分支中的类型:

function example(x: string | number | boolean): void {
  if (typeof x === "string") {
    console.log(x.toUpperCase()); // x 是 string
    return;
  }
  
  // x 现在是 number | boolean
  
  if (typeof x === "number") {
    console.log(x.toFixed(2)); // x 是 number
    return;
  }
  
  // x 现在是 boolean
  console.log(x ? "yes" : "no");
}

赋值分析

let value: string | number;
 
value = "hello";
console.log(value.toUpperCase()); // value 是 string
 
value = 42;
console.log(value.toFixed(2)); // value 是 number

最佳实践

  1. 让 TypeScript 推断:不要过度注解,相信推断
  2. 使用类型守卫而非断言:类型守卫更安全
  3. 创建可复用的类型守卫:封装常用检查逻辑
  4. 使用穷尽检查:确保处理所有联合类型成员
  5. 避免双重断言:通常意味着类型设计有问题
// 推荐:可复用的类型守卫
function isNonEmptyArray<T>(arr: T[]): arr is [T, ...T[]] {
  return arr.length > 0;
}
 
function processArray(arr: string[]): void {
  if (isNonEmptyArray(arr)) {
    const first = arr[0]; // string,不是 string | undefined
  }
}

typescript 类型推断 类型守卫