Skip to content

第8章 类型守卫与类型收缩

本章定位:学习TypeScript如何在运行时安全地检查类型,以及如何在代码块中收缩类型范围,让TypeScript准确知道当前变量的类型。


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
}
}
  1. 运行时类型检查:确保值符合预期类型
  2. 类型收缩:缩小联合类型的范围
  3. 代码安全性:减少类型断言的使用
  4. 更好的IDE支持:在收缩后的代码块中获得准确的代码补全

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只能用于以下类型检查:

  • "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");
}
}
}
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());
}

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: ...")
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只能用于对象类型,不能用于原始类型:

// 错误用法
"hello" instanceof string; // 编译错误!string是原始类型
// 正确用法
const str = new String("hello");
str instanceof String; // true
// 对于原始类型,使用typeof
const s = "hello";
typeof s === "string"; // true

自定义类型守卫返回类型谓词(Type Predicate):

// 类型谓词:parameterName is Type
function 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()}`);
}
}
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 dataclass
from typing import Union
@dataclass
class Person:
name: str
@dataclass
class 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)”

可辨识联合是一种特殊的联合类型,其中每个成员都有一个共同的、独特的字面量属性(判别属性):

// 判别属性:kind
interface 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;
}
}
  1. 穷尽性检查:TypeScript检查所有可能的情况
  2. 清晰的代码结构:每个case处理特定类型
  3. 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}`);
}
}
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 dataclass
from typing import Generic, TypeVar
from datetime import datetime
T = TypeVar("T")
@dataclass
class Success(Generic[T]):
kind: str = "success"
data: T = None
timestamp: datetime = None
@dataclass
class Error:
kind: str = "error"
message: str = ""
code: int = 0
@dataclass
class 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..."

类型谓词返回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);
}
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());
}
}
}

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;
}
}
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;
}
}

如果将来添加新的Shape类型:

interface Triangle {
kind: "triangle";
base: number;
height: number;
}
type Shape = Circle | Square | Rectangle | Triangle;

旧的getArea函数会编译失败,因为default分支的shape类型变成了Triangle而非never。这正是穷尽性检查的价值——强制更新所有处理函数。

type ExcludeNever<T> = T extends never ? never : T;
// never被排除
type A = ExcludeNever<string | never>; // string
type B = ExcludeNever<never>; // never

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
}
}
function isUser(obj: unknown): obj is { name: string; age: number } {
return obj !== null && typeof obj === "object" && obj.hasOwnProperty("name") && obj.hasOwnProperty("age");
}
// 常见模式
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[]

本章学习了TypeScript的类型守卫与类型收缩:

  1. typeof守卫:

    • 用于原始类型检查
    • 支持string、number、boolean等
  2. instanceof守卫:

    • 用于类实例检查
    • 运行时检查对象原型链
  3. 自定义类型守卫:

    • 返回类型谓词value is Type
    • 适用于复杂类型检查
  4. 可辨识联合:

    • 使用共同字面量属性作为判别
    • 支持穷尽性检查
    • switch/case结构清晰
  5. never类型:

    • 表示不可能的状态
    • 用于穷尽性检查
    • 强制覆盖所有情况

编写函数,处理string | number | boolean,打印不同类型的描述。

创建Animal及其子类Dog、Cat,实现makeSound函数。

创建isValidUser函数,检查对象是否是有效的User结构。

创建APIResponse的可辨识联合,处理success、error、loading状态。

为可辨识联合添加新类型,确保旧代码编译失败。



下一章我们将学习TypeScript的异步编程,包括Promise、async/await、错误处理等,与Python的asyncio进行对比。