TypeScript 类型操作:类型推断、断言、收窄与兼容性

这一篇深入 TypeScript 的类型操作机制:类型推断的规则、类型断言的使用、
类型收窄的策略,以及类型兼容性的判断。

TypeScript 可以在很多地方自动推断类型,无需显式注解。

 1// 变量推断
 2let x = 3;        // x 的类型是 number
 3let y = 'hello'; // y 的类型是 string
 4
 5// 函数返回类型推断
 6function add(a: number, b: number) {
 7  return a + b; // 返回类型推断为 number
 8}
 9
10// 数组推断
11const arr = [1, 2, 3]; // arr 的类型是 number[]
12const mixed = [1, 'hello']; // mixed 的类型是 (number | string)[]
1// TypeScript 会推断出所有可能类型的联合类型
2const arr = [1, 'hello', true];
3// arr 的类型是 (number | string | boolean)[]
4
5// 如果希望推断为更具体的类型,需要显式注解
6const arr2: (number | string)[] = [1, 'hello'];
 1// 根据上下文推断类型
 2window.onmousedown = function(mouseEvent) {
 3  // mouseEvent 的类型被推断为 MouseEvent
 4  console.log(mouseEvent.button);
 5};
 6
 7// 数组方法的回调函数类型推断
 8const numbers = [1, 2, 3];
 9numbers.map(n => n * 2); // n 的类型推断为 number
10
11// 对象字面量的上下文推断
12interface ButtonProps {
13  onClick: (event: MouseEvent) => void;
14}
15
16const button: ButtonProps = {
17  onClick(event) {
18    // event 的类型推断为 MouseEvent
19    console.log(event.button);
20  }
21};
 1// 自动推断返回类型
 2function getValue() {
 3  return Math.random() > 0.5 ? 'hello' : 42;
 4}
 5// 返回类型推断为 string | number
 6
 7// 显式返回类型注解
 8function getValue2(): string | number {
 9  return Math.random() > 0.5 ? 'hello' : 42;
10}
11
12// 异步函数返回类型推断
13async function fetchData() {
14  const response = await fetch('/api/data');
15  return response.json();
16}
17// 返回类型推断为 Promise<any>
 1// 推断可能不够精确
 2function processArray(arr: number[]) {
 3  return arr.map(x => x * 2);
 4}
 5
 6const result = processArray([1, 2, 3]);
 7// result 的类型是 number[],但不知道具体长度
 8
 9// 使用 const 断言可以获得更精确的类型
10const tuple = [1, 2, 3] as const;
11// tuple 的类型是 readonly [1, 2, 3]

类型断言告诉 TypeScript 你比它更了解某个值的类型。

 1// 基础用法
 2const value: unknown = 'hello';
 3const str = value as string;
 4str.toUpperCase(); // 现在可以使用 string 的方法
 5
 6// 断言为更具体的类型
 7interface ApiResponse {
 8  data: {
 9    user: {
10      name: string;
11      age: number;
12    };
13  };
14}
15
16const response = {} as ApiResponse;
17response.data.user.name; // TypeScript 认为这是安全的
1// 另一种语法(在 JSX 中不能使用)
2const value = <string>'hello';
3const str = <string>value;
1// 有时候需要先断言为 any 或 unknown,再断言为目标类型
2const value: string = 'hello';
3const num = value as unknown as number;
4// 或者
5const num2 = value as any as number;
 1// 非空断言操作符 !
 2function processElement(element: HTMLElement | null) {
 3  // 使用非空断言,告诉 TypeScript 这里不会是 null
 4  element!.style.color = 'red';
 5}
 6
 7// 可选链和非空断言结合
 8function getValue(obj: { value?: string }) {
 9  return obj.value!.toUpperCase(); // 断言 value 一定存在
10}
 1// 类型断言不会进行运行时检查
 2const value: unknown = 'hello';
 3const num = value as number;
 4num.toFixed(2); // 编译通过,但运行时会报错
 5
 6// 更安全的做法:使用类型守卫
 7function isNumber(value: unknown): value is number {
 8  return typeof value === 'number';
 9}
10
11if (isNumber(value)) {
12  value.toFixed(2); // 类型安全
13}

类型收窄是 TypeScript 通过控制流分析,将联合类型缩小到更具体的类型。

1function process(value: string | number) {
2  if (typeof value === 'string') {
3    // 这里 value 的类型是 string
4    return value.toUpperCase();
5  } else {
6    // 这里 value 的类型是 number
7    return value.toFixed(2);
8  }
9}
 1class Dog {
 2  bark() {
 3    console.log('Woof!');
 4  }
 5}
 6
 7class Cat {
 8  meow() {
 9    console.log('Meow!');
10  }
11}
12
13function makeSound(animal: Dog | Cat) {
14  if (animal instanceof Dog) {
15    animal.bark(); // TypeScript 知道这里是 Dog
16  } else {
17    animal.meow(); // TypeScript 知道这里是 Cat
18  }
19}
 1interface Fish {
 2  swim: () => void;
 3}
 4
 5interface Bird {
 6  fly: () => void;
 7}
 8
 9function move(animal: Fish | Bird) {
10  if ('swim' in animal) {
11    animal.swim(); // TypeScript 知道这里是 Fish
12  } else {
13    animal.fly(); // TypeScript 知道这里是 Bird
14  }
15}
 1function process(value: string | null) {
 2  if (value === null) {
 3    return; // 这里 value 是 null
 4  }
 5  // 这里 value 是 string
 6  return value.toUpperCase();
 7}
 8
 9// 使用 == null 可以同时检查 null 和 undefined
10function process2(value: string | null | undefined) {
11  if (value == null) {
12    return; // 这里 value 是 null 或 undefined
13  }
14  // 这里 value 是 string
15  return value.toUpperCase();
16}
 1function process(value: string | null | undefined) {
 2  if (value) {
 3    // 这里 value 是 string(排除了 null 和 undefined)
 4    return value.toUpperCase();
 5  }
 6  // 这里 value 是 null 或 undefined
 7  return '';
 8}
 9
10// 注意:空字符串也是 falsy
11function process2(value: string | null) {
12  if (value) {
13    // value 是 string,但不包括空字符串
14    return value.toUpperCase();
15  }
16  // value 是 null 或空字符串
17}
 1interface User {
 2  name: string;
 3  email: string;
 4}
 5
 6interface Admin {
 7  name: string;
 8  role: 'admin';
 9  permissions: string[];
10}
11
12function isAdmin(user: User | Admin): user is Admin {
13  return 'role' in user && user.role === 'admin';
14}
15
16function processUser(user: User | Admin) {
17  if (isAdmin(user)) {
18    // TypeScript 知道 user 是 Admin
19    console.log(user.permissions);
20  } else {
21    // TypeScript 知道 user 是 User
22    console.log(user.email);
23  }
24}
 1type Success = {
 2  type: 'success';
 3  data: string;
 4};
 5
 6type Error = {
 7  type: 'error';
 8  message: string;
 9};
10
11type Result = Success | Error;
12
13function handleResult(result: Result) {
14  switch (result.type) {
15    case 'success':
16      // TypeScript 知道 result 是 Success
17      console.log(result.data);
18      break;
19    case 'error':
20      // TypeScript 知道 result 是 Error
21      console.error(result.message);
22      break;
23  }
24}
1let value: string | number;
2
3value = 'hello';
4// 这里 value 的类型是 string
5value.toUpperCase();
6
7value = 42;
8// 这里 value 的类型是 number
9value.toFixed(2);
 1function example() {
 2  let x: string | number | boolean;
 3  
 4  x = Math.random() < 0.5;
 5  
 6  if (Math.random() < 0.5) {
 7    x = 'hello';
 8    // 这里 x 的类型是 string
 9    return x.toUpperCase();
10  }
11  
12  x = 42;
13  // 这里 x 的类型是 number
14  return x.toFixed(2);
15}

TypeScript 使用结构化类型系统(Structural Typing),只要结构兼容,就认为类型兼容。

 1interface Point {
 2  x: number;
 3  y: number;
 4}
 5
 6interface NamedPoint {
 7  x: number;
 8  y: number;
 9  name: string;
10}
11
12let point: Point = { x: 1, y: 2 };
13let namedPoint: NamedPoint = { x: 1, y: 2, name: 'origin' };
14
15// Point 兼容 NamedPoint(NamedPoint 有 Point 的所有属性)
16point = namedPoint; // 正确
17
18// NamedPoint 不兼容 Point(Point 缺少 name 属性)
19// namedPoint = point; // 错误
20
21// 但可以这样赋值(对象字面量有额外检查)
22let p: Point = { x: 1, y: 2, name: 'origin' }; // 错误:多余属性
 1// 参数类型:目标函数的参数类型必须是源函数参数类型的超类型
 2let x = (a: number) => 0;
 3let y = (b: number, s: string) => 0;
 4
 5y = x; // 正确:y 的参数是 x 的参数的超集
 6// x = y; // 错误:x 的参数不是 y 的参数的超集
 7
 8// 返回类型:目标函数的返回类型必须是源函数返回类型的子类型
 9let x2 = () => ({ name: 'Alice' });
10let y2 = () => ({ name: 'Alice', location: 'Seattle' });
11
12x2 = y2; // 正确:x2 的返回类型是 y2 的返回类型的子类型
13// y2 = x2; // 错误:y2 的返回类型不是 x2 的返回类型的子类型
 1// 可选参数和必需参数兼容
 2let x = (a: number) => 0;
 3let y = (a?: number) => 0;
 4
 5x = y; // 正确
 6y = x; // 也正确(但调用 y 时可能缺少参数)
 7
 8// 剩余参数兼容固定参数
 9let x3 = (...args: number[]) => 0;
10let y3 = (a: number) => 0;
11
12x3 = y3; // 正确
13y3 = x3; // 也正确
 1enum Status {
 2  Open,
 3  Closed
 4}
 5
 6enum Color {
 7  Red,
 8  Blue
 9}
10
11let status: Status = Status.Open;
12// status = Color.Red; // 错误:枚举类型不兼容
13
14// 数字枚举和数字类型兼容
15let num: number = Status.Open; // 正确
 1class Animal {
 2  feet: number;
 3  constructor(name: string, numFeet: number) {
 4    this.feet = numFeet;
 5  }
 6}
 7
 8class Size {
 9  feet: number;
10  constructor(numFeet: number) {
11    this.feet = numFeet;
12  }
13}
14
15let a: Animal;
16let s: Size;
17
18a = s; // 正确:结构兼容
19s = a; // 正确:结构兼容
20
21// 但私有成员和受保护成员会影响兼容性
22class Animal2 {
23  private feet: number;
24  constructor(name: string, numFeet: number) {
25    this.feet = numFeet;
26  }
27}
28
29class Size2 {
30  private feet: number;
31  constructor(numFeet: number) {
32    this.feet = numFeet;
33  }
34}
35
36let a2: Animal2;
37let s2: Size2;
38
39// a2 = s2; // 错误:私有成员不兼容
40// s2 = a2; // 错误:私有成员不兼容
 1// 未指定类型参数的泛型类型兼容性
 2interface Empty<T> {}
 3
 4let x: Empty<number>;
 5let y: Empty<string>;
 6
 7x = y; // 正确:Empty<T> 的结构相同
 8
 9// 有成员时,类型参数会影响兼容性
10interface NotEmpty<T> {
11  data: T;
12}
13
14let x2: NotEmpty<number>;
15let y2: NotEmpty<string>;
16
17// x2 = y2; // 错误:data 的类型不兼容
 1// 数组是协变的(covariant)
 2let array1: Array<number> = [1, 2, 3];
 3let array2: Array<number | string> = array1; // 正确
 4
 5// 但函数参数是逆变的(contravariant)
 6type Handler = (value: number) => void;
 7type Handler2 = (value: number | string) => void;
 8
 9let handler: Handler = (n: number) => console.log(n);
10// let handler2: Handler2 = handler; // 错误:参数类型不兼容
11
12// 函数返回类型是协变的
13type Getter = () => number;
14type Getter2 = () => number | string;
15
16let getter: Getter = () => 42;
17let getter2: Getter2 = getter; // 正确:返回类型兼容
 1// 使用类型推断和类型守卫
 2async function fetchUser(id: string): Promise<User | null> {
 3  try {
 4    const response = await fetch(`/api/users/${id}`);
 5    if (!response.ok) {
 6      return null;
 7    }
 8    const data = await response.json();
 9    
10    // 使用类型守卫验证数据
11    if (isUser(data)) {
12      return data;
13    }
14    return null;
15  } catch (error) {
16    return null;
17  }
18}
19
20function isUser(data: unknown): data is User {
21  return (
22    typeof data === 'object' &&
23    data !== null &&
24    'id' in data &&
25    'name' in data &&
26    typeof (data as any).id === 'string' &&
27    typeof (data as any).name === 'string'
28  );
29}
 1type Result<T> = 
 2  | { success: true; data: T }
 3  | { success: false; error: string };
 4
 5function processResult<T>(result: Result<T>): T {
 6  if (result.success) {
 7    // TypeScript 知道这里有 data
 8    return result.data;
 9  } else {
10    // TypeScript 知道这里有 error
11    throw new Error(result.error);
12  }
13}
 1// 使用类型兼容性实现函数重载
 2function format(value: string): string;
 3function format(value: number): string;
 4function format(value: string | number): string {
 5  return String(value);
 6}
 7
 8// 调用时根据参数类型推断返回类型
 9const str = format('hello'); // str 的类型是 string
10const num = format(42);     // num 的类型是 string

TypeScript 的类型操作机制提供了强大的类型系统:

  • 类型推断:自动推断类型,减少显式注解,提高开发效率
  • 类型断言:在需要时告诉 TypeScript 更具体的类型,但要谨慎使用
  • 类型收窄:通过控制流分析,将联合类型缩小到更具体的类型
  • 类型兼容性:结构化类型系统,只要结构兼容就认为类型兼容

理解这些机制,可以:

  • 写出更简洁的代码(利用类型推断)
  • 更安全地处理类型(使用类型收窄而不是类型断言)
  • 理解为什么某些代码能编译通过或不能(类型兼容性)
  • 避免常见的类型错误

在实际开发中,应该:

  • 优先使用类型推断,只在必要时显式注解
  • 使用类型守卫而不是类型断言
  • 利用类型收窄让 TypeScript 自动推断更精确的类型
  • 理解类型兼容性规则,避免意外的类型错误