TypeScript 类型操作:类型推断、断言、收窄与兼容性
这一篇深入 TypeScript 的类型操作机制:类型推断的规则、类型断言的使用、
类型收窄的策略,以及类型兼容性的判断。
类型推断(Type Inference)
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]
类型断言(Type Assertions)
类型断言告诉 TypeScript 你比它更了解某个值的类型。
as 语法
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}
类型收窄(Type Narrowing)
类型收窄是 TypeScript 通过控制流分析,将联合类型缩小到更具体的类型。
typeof 收窄
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}
instanceof 收窄
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}
in 操作符收窄
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; // 正确:返回类型兼容
实际应用场景
类型安全的 API 调用
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 自动推断更精确的类型
- 理解类型兼容性规则,避免意外的类型错误