TypeScript 开发指南 / 09 - 类型收窄
类型收窄
类型收窄(Type Narrowing)是 TypeScript 中通过条件判断将宽泛类型收窄为更具体类型的过程。
控制流分析(Control Flow Analysis)
TypeScript 通过分析代码的控制流来自动收窄类型:
function example(value: string | number | boolean): void {
// 此处 value: string | number | boolean
if (typeof value === "string") {
// 此处 value: string
console.log(value.toUpperCase());
} else if (typeof value === "number") {
// 此处 value: number
console.log(value.toFixed(2));
} else {
// 此处 value: boolean
console.log(value ? "yes" : "no");
}
}
typeof 类型守卫
function padLeft(value: string | number, padding: string | number): string {
if (typeof padding === "number") {
// padding: number
return " ".repeat(padding) + value;
}
// padding: string
return padding + value;
}
// typeof 的所有有效返回值
type TypeofResult =
| "string"
| "number"
| "bigint"
| "boolean"
| "symbol"
| "undefined"
| "object"
| "function";
function process(value: unknown): void {
if (typeof value === "string") { /* string */ }
if (typeof value === "number") { /* number */ }
if (typeof value === "boolean") { /* boolean */ }
if (typeof value === "undefined") { /* undefined */ }
if (typeof value === "function") { /* Function */ }
if (typeof value === "object") { /* object | null */ }
}
注意:
typeof null返回"object",所以检查 null 需要额外判断。
真值收窄(Truthiness Narrowing)
function printAll(strs: string | string[] | null): void {
// 真值检查排除 null、undefined、""、0、NaN
if (strs && typeof strs === "object") {
// strs: string[](排除了 null)
for (const s of strs) {
console.log(s);
}
} else if (typeof strs === "string") {
// strs: string
console.log(strs);
}
}
// ⚠️ 注意:空字符串是 falsy 值
function test(value: string | null): void {
if (value) {
// value: string(但不包括空字符串 "")
console.log(value.toUpperCase());
}
// 如果需要包含空字符串,使用显式检查
if (value !== null) {
// value: string(包含空字符串)
}
}
相等性收窄(Equality Narrowing)
function example(x: string | number, y: string | boolean): void {
if (x === y) {
// x 和 y 都是 string(公共类型)
console.log(x.toUpperCase()); // ✅ string
console.log(y.toUpperCase()); // ✅ string
}
}
// 使用 switch
function switchExample(value: string | number | boolean): void {
switch (typeof value) {
case "string":
console.log(value.toUpperCase()); // string
break;
case "number":
console.log(value.toFixed(2)); // number
break;
case "boolean":
console.log(value ? "yes" : "no"); // boolean
break;
}
}
// null 检查
function nullCheck(value: string | null | undefined): void {
if (value != null) {
// value: string(排除 null 和 undefined)
console.log(value.toUpperCase());
}
if (value !== null && value !== undefined) {
// 同上,更明确
}
}
instanceof 类型守卫
function formatDate(value: string | Date): string {
if (value instanceof Date) {
// value: Date
return value.toISOString();
}
// value: string
return new Date(value).toISOString();
}
// 自定义类
class ApiError {
constructor(public status: number, public message: string) {}
}
class NetworkError {
constructor(public code: string) {}
}
function handleError(error: ApiError | NetworkError | Error): string {
if (error instanceof ApiError) {
return `API 错误: ${error.status} - ${error.message}`;
}
if (error instanceof NetworkError) {
return `网络错误: ${error.code}`;
}
return `未知错误: ${error.message}`;
}
in 操作符收窄
interface Bird {
fly(): void;
layEggs(): void;
}
interface Fish {
swim(): void;
layEggs(): void;
}
function move(animal: Bird | Fish): void {
if ("fly" in animal) {
// animal: Bird
animal.fly();
} else {
// animal: Fish
animal.swim();
}
}
// 可选属性
interface Circle { kind: "circle"; radius: number; }
interface Square { kind: "square"; sideLength: number; }
function area(shape: Circle | Square): number {
if ("radius" in shape) {
// shape: Circle
return Math.PI * shape.radius ** 2;
}
// shape: Square
return shape.sideLength ** 2;
}
可辨识联合收窄
// 每个成员都有共同的 discriminant 属性
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; sideLength: number }
| { kind: "rectangle"; width: number; height: number }
| { kind: "triangle"; base: number; height: number };
// 使用 switch 收窄
function getArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.sideLength ** 2;
case "rectangle":
return shape.width * shape.height;
case "triangle":
return 0.5 * shape.base * shape.height;
}
}
// 使用 if 收窄
function describe(shape: Shape): string {
if (shape.kind === "circle") {
return `圆,半径 ${shape.radius}`;
}
if (shape.kind === "square") {
return `正方形,边长 ${shape.sideLength}`;
}
// ...
}
自定义类型守卫(Type Predicates)
// 语法:parameterName is Type
function isString(value: unknown): value is string {
return typeof value === "string";
}
function isNumber(value: unknown): value is number {
return typeof value === "number" && !isNaN(value);
}
function isArray<T>(value: T | T[]): value is T[] {
return Array.isArray(value);
}
// 使用
function process(value: unknown): void {
if (isString(value)) {
console.log(value.toUpperCase()); // value: string
} else if (isNumber(value)) {
console.log(value.toFixed(2)); // value: number
}
}
复杂类型守卫
interface User {
id: number;
name: string;
email?: string;
}
interface Admin extends User {
role: "admin";
permissions: string[];
}
function isAdmin(user: User): user is Admin {
return "role" in user && (user as Admin).role === "admin";
}
function handleUser(user: User | Admin): void {
if (isAdmin(user)) {
// user: Admin
console.log(user.permissions);
} else {
// user: User
console.log(user.name);
}
}
断言函数(Assertion Functions)
// 断言函数:如果不满足条件则抛出错误
function assertIsDefined<T>(
value: T,
name: string
): asserts value is NonNullable<T> {
if (value === null || value === undefined) {
throw new Error(`Expected ${name} to be defined, got ${value}`);
}
}
function processValue(value: string | null | undefined): void {
assertIsDefined(value, "value");
// 这里 value: string
console.log(value.toUpperCase());
}
// 自定义断言
function assertIsNumber(
value: unknown,
name: string
): asserts value is number {
if (typeof value !== "number" || isNaN(value)) {
throw new Error(`${name} must be a valid number`);
}
}
function calculate(input: unknown): number {
assertIsNumber(input, "input");
// input: number
return input * 2;
}
收窄的链式传播
interface ApiResponse {
data: {
user: {
name: string;
address?: {
city: string;
zip?: string;
};
};
};
}
function getCity(response: ApiResponse): string {
// 需要逐层收窄
if (!response.data?.user?.address?.city) {
return "未知城市";
}
return response.data.user.address.city;
}
// 使用可选链 + 空值合并
function getCityV2(response: ApiResponse): string {
return response.data.user.address?.city ?? "未知城市";
}
业务场景:API 错误处理
// 使用可辨识联合处理 API 响应
type ApiResult<T> =
| { success: true; data: T; timestamp: number }
| { success: false; error: ApiError; timestamp: number };
interface ApiError {
code: number;
message: string;
details?: Record<string, string[]>;
}
interface User {
id: number;
name: string;
}
async function fetchUser(id: number): Promise<ApiResult<User>> {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
const error: ApiError = await response.json();
return { success: false, error, timestamp: Date.now() };
}
const data = await response.json();
return { success: true, data, timestamp: Date.now() };
} catch (e) {
return {
success: false,
error: { code: 0, message: "网络错误" },
timestamp: Date.now()
};
}
}
// 使用
const result = await fetchUser(1);
if (result.success) {
// result.data: User
console.log(`用户名: ${result.data.name}`);
} else {
// result.error: ApiError
console.error(`错误 ${result.error.code}: ${result.error.message}`);
}
常见陷阱
1. 闭包中的类型收窄丢失
function example(value: string | null): void {
if (value !== null) {
// value: string ✅
setTimeout(() => {
// ❌ value 可能为 null(闭包捕获了变量引用)
// console.log(value.toUpperCase());
// ✅ 解决方案:在收窄后赋值给局部常量
const safeValue = value;
console.log(safeValue.toUpperCase());
}, 1000);
}
}
2. 数组中的类型收窄
const mixed: (string | number)[] = [1, "two", 3, "four"];
// ❌ 不能直接用 typeof 过滤后获得类型安全的数组
const strings = mixed.filter(item => typeof item === "string");
// 类型仍然是 (string | number)[]
// ✅ 使用类型守卫
function isString(value: unknown): value is string {
return typeof value === "string";
}
const safeStrings = mixed.filter(isString);
// 类型是 string[]
3. 类型断言 vs 类型收窄
function bad(value: unknown): string {
// ❌ 类型断言:不安全,绕过检查
return (value as string).toUpperCase();
}
function good(value: unknown): string {
// ✅ 类型收窄:安全,编译器验证
if (typeof value === "string") {
return value.toUpperCase();
}
throw new Error("Expected string");
}
注意事项
- 优先使用类型收窄而非类型断言——更安全
- 可辨识联合是处理复杂联合类型的最佳模式
- 自定义类型守卫适用于无法通过内置方式收窄的场景
- 注意闭包中类型收窄可能丢失——必要时使用局部常量
- 空值检查是类型收窄的常见场景,使用
!= null同时检查null和undefined