说说 TypeScript 中的类型推断和类型断言?
问题解析
类型推断和类型断言是 TypeScript 类型系统的两个重要特性,考察候选人对编译器类型推断机制的理解,以及何时需要手动进行类型断言。
核心概念
- 类型推断(Type Inference):编译器自动推断变量类型
- 类型断言(Type Assertion):手动告诉编译器变量的类型
- 类型守卫(Type Guards):运行时类型检查,收窄类型范围
- 非空断言:
!标记值不为 null/undefined
详细解答
1. 类型推断
当类型没有给出时,TypeScript 编译器利用类型推断来推断类型。
1.1 基础类型推断
let str = "string"; // 推断为 string
let num = 123; // 推断为 number
let bool = true; // 推断为 boolean
1.2 上下文类型推断
// 根据上下文推断事件参数类型
window.onmousedown = function(event) {
console.log(event.button); // TypeScript 知道 event 是 MouseEvent
};
// 根据返回值推断
function createPerson() {
return { name: "Tom", age: 25 }; // 推断为 { name: string; age: number; }
}
1.3 最佳通用类型推断
let arr = [0, 1, null]; // 推断为 (number | null)[]
let zoo = [new Rhino(), new Elephant(), new Snake()];
// 推断为 (Rhino | Elephant | Snake)[]
// 如果有共同基类,会推断为基类数组
1.4 函数返回值推断
function add(a: number, b: number) {
return a + b; // 推断返回值为 number
}
function greet(name: string) {
return `Hello, ${name}`; // 推断返回值为 string
}
1.5 泛型类型推断
function identity<T>(arg: T): T {
return arg;
}
const num = identity(123); // T 被推断为 number
const str = identity("hello"); // T 被推断为 string
2. 类型断言
类型断言好比是其他语言里的类型转换,但是不进行特殊的数据检查和解构。它没有运行时的影响,只是在编译阶段起作用。
2.1 语法
TypeScript 提供了两种语法进行类型断言:
// 尖括号语法(在 JSX 中不能使用)
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
// as 语法(推荐,JSX 兼容)
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
2.2 应用场景
场景一:处理 any 类型
function getLength(something: string | number): number {
if ((something as string).length) {
return (something as string).length;
} else {
return something.toString().length;
}
}
场景二:DOM 操作
const canvas = document.getElementById('canvas');
// canvas 类型是 HTMLElement | null
const canvas = document.getElementById('canvas') as HTMLCanvasElement;
// 现在可以使用 Canvas 特有的方法
canvas.getContext('2d');
场景三:处理联合类型
interface Cat {
name: string;
run(): void;
}
interface Fish {
name: string;
swim(): void;
}
function getPet(): Cat | Fish {
// ...
}
const pet = getPet();
(pet as Fish).swim(); // 断言为 Fish 类型
2.3 双重断言
类型断言的层次可以连续:
// 先断言为 any,再断言为目标类型
const str: string = 'hello';
const num = (str as any) as number; // 不推荐,破坏了类型安全
警告:双重断言可能会破坏类型安全,应尽量避免。
3. 非空断言
在变量名后添加 ! 表示该值不为 null 或 undefined:
function processEntity(e?: Entity) {
let s = e!.name; // 断言 e 不为 undefined
}
// 与可选链操作符的区别
const name = entity?.name; // 如果 entity 为 null/undefined,返回 undefined
const name = entity!.name; // 断言 entity 一定存在,直接访问 name
4. 确定赋值断言
TypeScript 4.0+ 引入,允许在声明变量时断言该变量会被赋值:
let x!: number; // 确定赋值断言
initialize();
console.log(x + x); // 不会报错
function initialize() {
x = 10;
}
5. const 断言
TypeScript 3.4+ 引入,将表达式标记为不可变:
// 普通推断
let x = 'hello' as 'hello'; // 类型为 'hello'
// const 断言
let y = 'hello' as const; // 类型为 'hello'
// 对象字面量
let obj = {
x: 10,
y: [20, 30]
} as const;
// obj.x 类型为 10(不是 number)
// obj.y 类型为 readonly [20, 30]
深入理解
类型推断的局限性
// 推断过于宽泛
const config = {
host: 'localhost',
port: 3000
}; // 推断为 { host: string; port: number; }
// 使用 as const 获得更精确的类型
const config = {
host: 'localhost',
port: 3000
} as const; // 推断为 { readonly host: 'localhost'; readonly port: 3000; }
// 或者显式注解
const config: { host: string; port: number } = {
host: 'localhost',
port: 3000
};
类型断言的安全使用
// 不安全的断言
const input = document.getElementById('input') as HTMLInputElement;
// 如果元素不存在,运行时报错
// 安全的断言
const input = document.getElementById('input');
if (input) {
(input as HTMLInputElement).value = 'hello';
}
// 或者使用类型守卫
function isInputElement(el: HTMLElement | null): el is HTMLInputElement {
return el !== null && el.tagName === 'INPUT';
}
if (isInputElement(input)) {
input.value = 'hello'; // 类型已收窄
}
最佳实践
- 优先依赖类型推断:减少不必要的类型注解,让代码更简洁
- 谨慎使用类型断言:
- 只在必要时使用
- 确保断言的正确性
- 避免双重断言
- 使用类型守卫替代断言:运行时检查比编译时断言更安全
- 非空断言要谨慎:确保值确实不为 null/undefined
- as const 用于常量:获得更精确的字面量类型
面试要点
- 类型推断:编译器自动推断变量类型,无需显式注解
- 推断场景:
- 变量初始化
- 函数返回值
- 泛型参数
- 上下文类型
- 类型断言:手动指定类型,两种语法
<Type>和as Type - 应用场景:
- 处理 any 类型
- DOM 元素类型
- 联合类型收窄
- 非空断言:
!标记值不为 null/undefined - const 断言:
as const创建只读字面量类型 - 安全性:类型断言不进行运行时检查,需谨慎使用
- 与类型守卫的区别:断言是编译时行为,守卫是运行时检查