类型推断与类型守卫
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最佳实践
- 让 TypeScript 推断:不要过度注解,相信推断
- 使用类型守卫而非断言:类型守卫更安全
- 创建可复用的类型守卫:封装常用检查逻辑
- 使用穷尽检查:确保处理所有联合类型成员
- 避免双重断言:通常意味着类型设计有问题
// 推荐:可复用的类型守卫
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
}
}