第8章 类型守卫与类型收缩
本章定位:学习TypeScript如何在运行时安全地检查类型,以及如何在代码块中收缩类型范围,让TypeScript准确知道当前变量的类型。
8.1 类型守卫的概念
Section titled “8.1 类型守卫的概念”什么是类型收缩
Section titled “什么是类型收缩”TypeScript的类型检查是编译时的,但代码执行是运行时的。类型守卫(Type Guard)是运行时检查类型的机制,通过检查,TypeScript能在代码块中推断出更具体的类型。
function process(value: string | number) { // 没有类型检查时,value可以是string或number if (typeof value === "string") { // TypeScript知道这里value是string console.log(value.toUpperCase()); // OK } else { // TypeScript知道这里value是number console.log(value.toFixed(2)); // OK }}类型守卫的作用
Section titled “类型守卫的作用”- 运行时类型检查:确保值符合预期类型
- 类型收缩:缩小联合类型的范围
- 代码安全性:减少类型断言的使用
- 更好的IDE支持:在收缩后的代码块中获得准确的代码补全
8.2 typeof 类型守卫
Section titled “8.2 typeof 类型守卫”typeof是最简单的类型守卫:
function print(value: string | number | boolean) { if (typeof value === "string") { console.log(`String: ${value.toUpperCase()}`); } else if (typeof value === "number") { console.log(`Number: ${value.toFixed(2)}`); } else { console.log(`Boolean: ${value}`); }}typeof 支持的类型
Section titled “typeof 支持的类型”typeof只能用于以下类型检查:
"string""number""bigint""boolean""symbol""undefined""function""object"
// typeof 陷阱function check(value: object) { if (typeof value === "object") { // 这里value仍然是object,不缩小 // 无法访问具体属性 // value.length // 错误! }}
// 更好的方式function check(value: string[] | object) { if (typeof value === "object") { // 对于数组和普通对象都返回"object" if (Array.isArray(value)) { console.log(`Array with ${value.length} elements`); } else { console.log("Plain object"); } }}typeof 与 undefined
Section titled “typeof 与 undefined”function process(value: string | undefined) { if (typeof value === "string") { // OK console.log(value.toUpperCase()); } else { // value是undefined console.log("value is undefined"); }}
// 或者直接比较if (value !== undefined) { console.log(value.toUpperCase());}8.3 instanceof 类型守卫
Section titled “8.3 instanceof 类型守卫”instanceof用于检查对象是否是某个类的实例:
class Animal { name: string;
constructor(name: string) { this.name = name; }}
class Dog extends Animal { breed: string;
constructor(name: string, breed: string) { super(name); this.breed = breed; }}
class Cat extends Animal { color: string;
constructor(name: string, color: string) { super(name); this.color = color; }}
function makeSound(animal: Animal) { if (animal instanceof Dog) { console.log(`${animal.name} says: Woof! (${animal.breed})`); } else if (animal instanceof Cat) { console.log(`${animal.name} says: Meow! (${animal.color})`); } else { console.log(`${animal.name} says: ...`); }}Python对比:
class Animal: def __init__(self, name: str): self.name = name
class Dog(Animal): def __init__(self, name: str, breed: str): super().__init__(name) self.breed = breed
def make_sound(animal: Animal) -> None: if isinstance(animal, Dog): print(f"{animal.name} says: Woof! ({animal.breed})") elif isinstance(animal, Cat): print(f"{animal.name} says: Meow!") else: print(f"{animal.name} says: ...")instanceof 与构造函数
Section titled “instanceof 与构造函数”interface Point { x: number; y: number;}
class CustomPoint implements Point { constructor(public x: number, public y: number) {}}
function processPoint(point: Point | CustomPoint) { if (point instanceof CustomPoint) { // point是CustomPoint console.log(`Custom point at (${point.x}, ${point.y})`); } else { // point是普通Point console.log(`Point at (${point.x}, ${point.y})`); }}instanceof 的局限性
Section titled “instanceof 的局限性”instanceof只能用于对象类型,不能用于原始类型:
// 错误用法"hello" instanceof string; // 编译错误!string是原始类型
// 正确用法const str = new String("hello");str instanceof String; // true
// 对于原始类型,使用typeofconst s = "hello";typeof s === "string"; // true8.4 自定义类型守卫
Section titled “8.4 自定义类型守卫”函数返回类型谓词
Section titled “函数返回类型谓词”自定义类型守卫返回类型谓词(Type Predicate):
// 类型谓词:parameterName is Typefunction isString(value: unknown): value is string { return typeof value === "string";}
function process(value: unknown) { if (isString(value)) { // TypeScript知道value是string console.log(value.toUpperCase()); }}interface User { name: string; age: number;}
interface Product { id: string; price: number;}
function isUser(obj: unknown): obj is User { return ( typeof obj === "object" && obj !== null && "name" in obj && "age" in obj && typeof (obj as User).name === "string" && typeof (obj as User).age === "number" );}
function isProduct(obj: unknown): obj is Product { return ( typeof obj === "object" && obj !== null && "id" in obj && "price" in obj && typeof (obj as Product).id === "string" && typeof (obj as Product).price === "number" );}
function handleEntity(entity: User | Product | string) { if (isUser(entity)) { console.log(`User: ${entity.name}, ${entity.age} years old`); } else if (isProduct(entity)) { console.log(`Product: ${entity.id}, $${entity.price}`); } else { console.log(`String: ${entity.toUpperCase()}`); }}类型守卫与数组
Section titled “类型守卫与数组”interface Person { name: string;}
interface Company { name: string; employees: Person[];}
function isCompany(obj: unknown): obj is Company { return ( typeof obj === "object" && obj !== null && "employees" in obj && Array.isArray((obj as Company).employees) );}
const data: (Person | Company)[] = [ { name: "Alice" }, { name: "Bob" }, { name: "Corp", employees: [{ name: "Charlie" }, { name: "Diana" }] }];
for (const item of data) { if (isCompany(item)) { console.log(`${item.name} has ${item.employees.length} employees`); } else { console.log(`Person: ${item.name}`); }}Python对比:
from dataclasses import dataclassfrom typing import Union
@dataclassclass Person: name: str
@dataclassclass Company: name: str employees: list[Person]
def is_company(obj) -> bool: return isinstance(obj, Company)
for item in data: if is_company(item): print(f"{item.name} has {len(item.employees)} employees") else: print(f"Person: {item.name}")8.5 区分联合(discriminated unions)
Section titled “8.5 区分联合(discriminated unions)”可辨识联合是一种特殊的联合类型,其中每个成员都有一个共同的、独特的字面量属性(判别属性):
// 判别属性:kindinterface Circle { kind: "circle"; radius: number;}
interface Square { kind: "square"; side: number;}
interface Rectangle { kind: "rectangle"; width: number; height: number;}
type Shape = Circle | Square | Rectangle;
function area(shape: Shape): number { switch (shape.kind) { case "circle": return Math.PI * shape.radius ** 2; case "square": return shape.side ** 2; case "rectangle": return shape.width * shape.height; }}判别属性的优势
Section titled “判别属性的优势”- 穷尽性检查:TypeScript检查所有可能的情况
- 清晰的代码结构:每个case处理特定类型
- IDE自动补全:在每个case中获得正确的属性
// 穷尽性检查function describe(shape: Shape): string { switch (shape.kind) { case "circle": return `Circle with radius ${shape.radius}`; case "square": return `Square with side ${shape.side}`; case "rectangle": return `Rectangle ${shape.width}x${shape.height}`; default: // 如果漏掉某个case,TypeScript会报错 const _exhaustive: never = shape; throw new Error(`Unknown shape: ${_exhaustive}`); }}嵌套可辨识联合
Section titled “嵌套可辨识联合”interface Success<T> { kind: "success"; data: T; timestamp: Date;}
interface Error { kind: "error"; message: string; code: number;}
interface Loading { kind: "loading";}
type Result<T> = Success<T> | Error | Loading;
function handleResult<T>(result: Result<T>): string { switch (result.kind) { case "success": return `Success at ${result.timestamp}: ${JSON.stringify(result.data)}`; case "error": return `Error ${result.code}: ${result.message}`; case "loading": return "Loading..."; }}Python对比:
from dataclasses import dataclassfrom typing import Generic, TypeVarfrom datetime import datetime
T = TypeVar("T")
@dataclassclass Success(Generic[T]): kind: str = "success" data: T = None timestamp: datetime = None
@dataclassclass Error: kind: str = "error" message: str = "" code: int = 0
@dataclassclass Loading: kind: str = "loading"
def handle_result(result): if result.kind == "success": return f"Success: {result.data}" elif result.kind == "error": return f"Error {result.code}: {result.message}" elif result.kind == "loading": return "Loading..."8.6 类型谓词(type predicates)
Section titled “8.6 类型谓词(type predicates)”类型谓词返回parameterName is Type:
function isNumber(value: unknown): value is number { return typeof value === "number";}
function isString(value: unknown): value is string { return typeof value === "string";}
function isArray(value: unknown): value is unknown[] { return Array.isArray(value);}类型谓词与泛型
Section titled “类型谓词与泛型”interface Fish { swim(): void;}
interface Bird { fly(): void;}
function isFish(pet: Fish | Bird): pet is Fish { return (pet as Fish).swim !== undefined;}
function getPet(): Fish | Bird { // ...}
const pet = getPet();if (isFish(pet)) { pet.swim(); // TypeScript知道pet是Fish} else { pet.fly(); // TypeScript知道pet是Bird}function isNonNull<T>(value: T | null | undefined): value is T { return value !== null && value !== undefined;}
function isStringArray(value: unknown): value is string[] { return ( Array.isArray(value) && value.every(item => typeof item === "string") );}
function process(data: string | string[] | null | undefined) { if (isNonNull(data)) { // data不是null或undefined了 if (isStringArray(data)) { // data是string[] data.forEach(s => console.log(s.toUpperCase())); } else { // data是string console.log(data.toUpperCase()); } }}8.7 never类型与穷尽性检查
Section titled “8.7 never类型与穷尽性检查”never 类型
Section titled “never 类型”never表示永远不可能达到的状态:
// 函数永不返回function throwError(message: string): never { throw new Error(message);}
// 类型收窄到不可能function exhaustiveCheck(x: string | number) { if (typeof x === "string") { console.log("string"); } else if (typeof x === "number") { console.log("number"); } else { // x是never,因为所有可能情况都已处理 const _exhaustive: never = x; }}穷尽性检查模式
Section titled “穷尽性检查模式”type Shape = Circle | Square | Rectangle;
function getArea(shape: Shape): number { switch (shape.kind) { case "circle": return Math.PI * shape.radius ** 2; case "square": return shape.side ** 2; case "rectangle": return shape.width * shape.height; default: // 确保覆盖所有情况 const _check: never = shape; return _check; }}添加新类型时的穷尽检查
Section titled “添加新类型时的穷尽检查”如果将来添加新的Shape类型:
interface Triangle { kind: "triangle"; base: number; height: number;}
type Shape = Circle | Square | Rectangle | Triangle;旧的getArea函数会编译失败,因为default分支的shape类型变成了Triangle而非never。这正是穷尽性检查的价值——强制更新所有处理函数。
never 与条件类型
Section titled “never 与条件类型”type ExcludeNever<T> = T extends never ? never : T;// never被排除
type A = ExcludeNever<string | never>; // stringtype B = ExcludeNever<never>; // never8.8 类型收缩的常用模式
Section titled “8.8 类型收缩的常用模式”in 操作符
Section titled “in 操作符”interface A { a: string;}
interface B { b: number;}
function process(obj: A | B) { if ("a" in obj) { console.log(obj.a); // obj是A } else { console.log(obj.b); // obj是B }}hasOwnProperty
Section titled “hasOwnProperty”function isUser(obj: unknown): obj is { name: string; age: number } { return obj !== null && typeof obj === "object" && obj.hasOwnProperty("name") && obj.hasOwnProperty("age");}简化检查函数
Section titled “简化检查函数”// 常见模式function isDefined<T>(value: T | undefined | null): value is T { return value !== undefined && value !== null;}
function isPositiveNumber(value: unknown): value is number { return typeof value === "number" && value > 0;}
// 使用const items: (string | undefined)[] = ["a", undefined, "b", undefined, "c"];
const definedItems = items.filter(isDefined); // string[]
const positiveNumbers: (number | undefined)[] = [1, undefined, -5, 3, undefined, -1];const positives = positiveNumbers.filter(isPositiveNumber); // number[]8.9 本章小结
Section titled “8.9 本章小结”本章学习了TypeScript的类型守卫与类型收缩:
-
typeof守卫:
- 用于原始类型检查
- 支持string、number、boolean等
-
instanceof守卫:
- 用于类实例检查
- 运行时检查对象原型链
-
自定义类型守卫:
- 返回类型谓词
value is Type - 适用于复杂类型检查
- 返回类型谓词
-
可辨识联合:
- 使用共同字面量属性作为判别
- 支持穷尽性检查
- switch/case结构清晰
-
never类型:
- 表示不可能的状态
- 用于穷尽性检查
- 强制覆盖所有情况
练习8.1:typeof守卫
Section titled “练习8.1:typeof守卫”编写函数,处理string | number | boolean,打印不同类型的描述。
练习8.2:instanceof守卫
Section titled “练习8.2:instanceof守卫”创建Animal及其子类Dog、Cat,实现makeSound函数。
练习8.3:自定义类型守卫
Section titled “练习8.3:自定义类型守卫”创建isValidUser函数,检查对象是否是有效的User结构。
练习8.4:可辨识联合
Section titled “练习8.4:可辨识联合”创建APIResponse的可辨识联合,处理success、error、loading状态。
练习8.5:穷尽性检查
Section titled “练习8.5:穷尽性检查”为可辨识联合添加新类型,确保旧代码编译失败。
下一章我们将学习TypeScript的异步编程,包括Promise、async/await、错误处理等,与Python的asyncio进行对比。