TypeScript深度强化第一天

679 阅读19分钟

TypeScript深度强化第一天

从JavaScript到TypeScript的蜕变之旅,掌握静态类型系统的精髓

🎯 学习目标

  • 理解TypeScript的核心价值和静态类型检查机制
  • 掌握TypeScript的基础类型系统
  • 学会使用联合类型、字面量类型等高级特性
  • 熟练运用函数类型注解和对象类型定义
  • 了解类型缩小和类型断言的使用场景

1. TypeScript的静态类型革命

1.1 为什么需要TypeScript?

什么是动态类型语言的痛点?

JavaScript就像是一个"什么都能装"的万能袋子,你可以往里面放任何东西。但是问题来了:

  • 运行时才知道出错:就像你打开袋子才发现里面装的不是你想要的东西
  • 拼写错误很难发现:比如你写了user.nam而不是user.name,JavaScript不会提醒你
  • 改代码很危险:就像改房子的地基,你不知道会影响到哪些地方
  • 代码像天书:别人(甚至是几个月后的你)看你的代码时,不知道变量里装的是什么
  • 团队合作容易出错:每个人对同一个数据的理解可能不一样
// JavaScript中常见的运行时错误
function calculatePrice(item) {
    return item.price * item.quantity;
}

const product = {
    name: "MacBook Pro",
    price: 2999,
    // 忘记添加 quantity 属性
};

console.log(calculatePrice(product)); // NaN - 运行时才发现错误

// 更多常见的JavaScript问题
function processUser(user) {
    console.log(user.toUpperCase()); // 如果user不是string,运行时报错
}

// 拼写错误
const user = { name: "张三", age: 25 };
console.log(user.nam); // undefined,很难发现拼写错误

TypeScript是怎么解决这些问题的?

TypeScript就像是给万能袋子贴上了标签,告诉你这个袋子里应该装什么东西。如果你装错了,它会在你打开袋子之前就提醒你:

interface Product {
    name: string;
    price: number;
    quantity: number;
}

function calculatePrice(item: Product): number {
    return item.price * item.quantity;
}

const product: Product = {
    name: "MacBook Pro",
    price: 2999,
    // TypeScript会提示缺少quantity属性
};

// TypeScript能捕获的错误类型
interface User {
    name: string;
    age: number;
}

function processUser(user: User): void {
    // TypeScript会阻止这种错误
    // console.log(user.toUpperCase()); // 错误:User类型没有toUpperCase方法
    console.log(user.name.toUpperCase()); // 正确
}

1.2 静态类型检查的优势

1.2.1 编译时错误检测

什么是编译时错误检测?

编译时错误检测就像是一个"超级检查员",在你的代码运行之前,它会先检查一遍:

  • 参数类型检查:就像检查你给函数传的参数是不是对的,比如需要数字你却传了字符串
  • 返回值类型验证:检查函数返回的东西是不是你期望的
  • 方法存在性检查:检查你调用的方法是不是真的存在,避免"找不到方法"的错误
  • 拼写错误检测:就像拼写检查器一样,发现你写错的属性名
// 类型安全的函数调用
function formatUserInfo(name: string, age: number): string {
    return `用户:${name},年龄:${age}岁`;
}

// 正确的调用
console.log(formatUserInfo("张三", 25));

// TypeScript会在编译时阻止错误的调用
// formatUserInfo(25, "张三"); // 参数类型错误
// formatUserInfo("李四"); // 参数数量不匹配

// 方法调用错误检测
const text = "Hello World";
// console.log(text.toLowercase()); // 错误:方法名拼写错误
console.log(text.toLowerCase()); // 正确
1.2.2 更好的代码智能提示

什么是代码智能提示?

代码智能提示就像是一个"超级助手",当你写代码的时候,它会主动帮你:

  • 精确的自动补全:就像打字时的联想输入,IDE知道你的对象有哪些属性和方法
  • 参数提示:当你调用函数时,它会告诉你需要传什么参数
  • 快速文档:鼠标放在代码上,就能看到这个东西是干什么的
  • 重构支持:改变量名时,它会帮你把所有用到这个变量的地方都改掉
  • 导航功能:点击就能跳转到定义这个类型的地方
interface DatabaseUser {
    id: number;
    username: string;
    email: string;
    createdAt: Date;
    profile: {
        firstName: string;
        lastName: string;
        avatar?: string;
    };
}

function displayUserInfo(user: DatabaseUser): void {
    // IDE会提供精确的代码补全
    console.log(user.profile.firstName); // 自动提示profile下的属性
    console.log(user.createdAt.getFullYear()); // 自动提示Date类型的方法
}
1.2.3 重构安全性

大型项目重构的安全保障

在大型项目中,代码重构是家常便饭。TypeScript的类型系统为重构提供了强大的安全保障:

  • 接口变更检测:修改接口时,所有使用该接口的地方都会被标记
  • 依赖关系追踪:清晰地了解代码之间的依赖关系
  • 批量重命名:可以安全地重命名类型、变量和函数
  • 影响范围分析:快速了解修改的影响范围
  • 向后兼容性检查:确保API变更不会破坏现有代码
// 当我们修改接口定义时,TypeScript会帮助我们找到所有需要更新的地方
interface ApiResponse {
    success: boolean;
    data: any;
    // 添加新字段时,TypeScript会提示哪些地方需要处理
    errorCode?: string;
}

function handleResponse(response: ApiResponse): void {
    if (response.success) {
        console.log("成功:", response.data);
    } else {
        // 如果我们添加了errorCode字段,这里可能需要更新
        console.log("失败:", response.errorCode || "未知错误");
    }
}

1.3 TypeScript的编译过程

1.3.1 类型擦除(Type Erasure)

什么是类型擦除?

类型擦除就像是"隐形墨水",你写代码的时候能看到类型信息,但是代码运行的时候,这些类型信息就消失了:

  • 零运行时开销:类型信息不会让你的程序变慢或变大
  • 纯JavaScript输出:最终运行的还是普通的JavaScript代码
  • 类型安全保证:写代码时有类型保护,运行时保持JavaScript的速度
  • 渐进式采用:你可以一点一点地给现有的JavaScript项目加上类型
// TypeScript源码
function greet(name: string): string {
    return `Hello, ${name}!`;
}

// 编译后的JavaScript
function greet(name) {
    return `Hello, ${name}!`;
}

// 类型注解在运行时完全消失
interface User {
    name: string;
    age: number;
}

// 编译后接口定义完全消失,不会产生任何JavaScript代码
1.3.2 编译目标和降级

兼容不同JavaScript版本的能力

TypeScript不仅提供类型检查,还能将现代JavaScript语法转换为兼容旧版本的代码:

  • 目标版本选择:可以选择编译到ES3、ES5、ES2015等不同版本
  • 语法转换:将新语法转换为兼容的旧语法
  • Polyfill支持:为缺失的API提供兼容性实现
  • 浏览器兼容性:确保代码能在目标浏览器中正常运行
// 现代TypeScript代码
const users = ["Alice", "Bob", "Charlie"];
const upperCaseNames = users.map(name => name.toUpperCase());

// 可选链操作符
const user = { profile: { name: "张三" } };
console.log(user.profile?.name);

// 编译为ES5时的输出
var users = ["Alice", "Bob", "Charlie"];
var upperCaseNames = users.map(function(name) {
    return name.toUpperCase();
});

// 可选链会被转换为安全的属性访问
var _a;
console.log((_a = user.profile) === null || _a === void 0 ? void 0 : _a.name);

2. TypeScript基础类型深度解析

什么是类型系统?

类型系统就像是给每个数据贴标签,告诉你这个数据是什么类型的。比如数字就贴"数字"标签,文字就贴"文字"标签。TypeScript的类型系统就是在JavaScript的基础上,给每种数据都贴上了标签。

2.1 原始类型的正确使用

什么是原始类型?

原始类型就像是最基本的乐高积木块,所有复杂的东西都是用这些基本块搭建的。JavaScript有7种基本积木块:

  • string:文字类型,比如"你好"
  • number:数字类型,比如123、3.14
  • boolean:布尔类型,只有true(真)和false(假)
  • null:空值
  • undefined:未定义
  • symbol:唯一标识符(比较少用)
  • bigint:超大数字(比较少用)

注意:函数(function)和对象(object)不是原始类型,它们是"复合类型",就像用多个积木块组合起来的。

2.1.1 string类型

什么是string类型?

string类型就是用来存放文字的,就像一个文字盒子。TypeScript对文字盒子的支持很全面:

  • 多种字符串格式:可以用单引号'、双引号"或者反引号`来包围文字
  • 类型安全的方法调用:TypeScript知道文字有哪些操作,比如变大写、获取长度等
  • 模板字符串类型推断:当你用反引号`写模板字符串时,TypeScript能猜出结果是什么类型
  • 避免混淆:区分普通的string和String对象(用小写的string就对了)
// 基础字符串类型
let userName: string = "TypeScript学习者";
let message: string = '单引号字符串';
let template: string = `模板字符串,用户:${userName}`;

// 字符串的方法调用是类型安全的
console.log(userName.length); // TypeScript知道string有length属性
console.log(userName.toUpperCase()); // 自动提示string的方法

// 避免使用包装类型
// let wrongName: String = "错误示例"; // String是对象类型,不是原始类型
let correctName: string = "正确示例"; // 使用小写的string
2.1.2 number类型

什么是number类型?

number类型就是用来存放数字的,就像一个数字盒子。JavaScript的数字盒子比较特别:

  • 统一的数字类型:不管是整数(1, 2, 3)还是小数(3.14, 2.5),都用number
  • 多进制表示:可以用不同的方式写数字,比如十进制(42)、十六进制(0x2A)等
  • 特殊数值处理:还能装一些特殊的"数字",比如无穷大(Infinity)、非数字(NaN)
  • BigInt支持:如果数字太大,可以用bigint类型
  • 精度考虑:JavaScript的数字有精度限制,很大或很小的数字可能不准确
// 数字类型支持各种进制
let decimal: number = 42;
let hex: number = 0x2A;
let binary: number = 0b101010;
let octal: number = 0o52;

// 特殊数值
let infinity: number = Infinity;
let negativeInfinity: number = -Infinity;
let notANumber: number = NaN;

// 数字方法的类型安全调用
let price = 19.99;
console.log(price.toFixed(2)); // "19.99"
console.log(price.toString()); // "19.99"

// BigInt类型(ES2020+)
let bigNumber: bigint = 9007199254740991n;
let anotherBig: bigint = BigInt(9007199254740991);
2.1.3 boolean类型

什么是boolean类型?

boolean类型就像一个开关,只有两种状态:开(true)和关(false)。TypeScript对这个开关要求很严格:

  • 严格的布尔值:只认true和false,不像JavaScript那样把其他值当作真假
  • 类型安全的逻辑运算:当你用&&、||这些逻辑运算时,TypeScript知道结果是什么类型
  • 避免隐式转换:防止你不小心把数字0当作false来用
  • 条件判断优化:在if语句中,TypeScript能更好地判断类型
// 布尔类型
let isActive: boolean = true;
let isCompleted: boolean = false;

// 布尔值的逻辑运算
let canAccess: boolean = isActive && !isCompleted;

// 避免隐式类型转换混淆
function checkStatus(active: boolean): string {
    // 明确的布尔值比较
    if (active === true) {
        return "激活状态";
    }
    return "非激活状态";
}

// 错误示例:不要将其他类型当作boolean使用
// checkStatus(1); // 错误:number不能赋值给boolean
// checkStatus("true"); // 错误:string不能赋值给boolean

2.2 类型推断机制

什么是类型推断?

类型推断就像是一个"智能猜测器",你不用告诉TypeScript变量是什么类型,它能自己猜出来:

  • 初始值推断:看你给变量赋的初始值,就能猜出类型。比如let name = "张三",它就知道name是string
  • 上下文推断:看你在什么地方用这个变量,就能猜出类型
  • 最佳公共类型:如果数组里有不同类型的值,它会找个通用的类型
  • 控制流分析:在if-else语句中,它能猜出不同分支里变量的类型
  • 函数返回值推断:看你return什么,就知道函数返回什么类型
// TypeScript的类型推断
let inferredString = "TypeScript"; // 推断为string类型
let inferredNumber = 42; // 推断为number类型
let inferredBoolean = true; // 推断为boolean类型

// 复杂的类型推断
let user = {
    name: "张三",  // string
    age: 25,      // number
    active: true  // boolean
}; // 推断为 { name: string; age: number; active: boolean; }

// 函数返回类型推断
function createUser(name: string, age: number) {
    return { name, age, id: Math.random() };
} // 返回类型被推断为 { name: string; age: number; id: number; }

// 最佳通用类型推断
let numbers = [1, 2, 3]; // 推断为number[]
let mixed = [1, "hello", true]; // 推断为(string | number | boolean)[]

2.3 数组类型的多种写法

什么是数组类型?

数组就像是一个排队的盒子,里面可以装很多同类型的东西。TypeScript为数组提供了很多种定义方式,确保你操作数组时不会出错。

2.3.1 数组类型声明

怎么告诉TypeScript数组里装的是什么?

TypeScript有两种方式来说明数组里装的是什么类型:

  • T[]语法:比如string[],意思是"装字符串的数组",简单好理解
  • Array语法:比如Array<string>,意思一样,但在复杂情况下更清楚
  • 性能考虑:两种写法最终效果一样,用哪个都行
// 数组类型的两种声明方式
let fruits: string[] = ["苹果", "香蕉", "橙子"];
let numbers: Array<number> = [1, 2, 3, 4, 5];

// 两种写法完全等价,但推荐使用第一种(更简洁)
let userNames: string[] = ["Alice", "Bob"];
let userAges: Array<number> = [25, 30]; // 也可以,但不如上面简洁
2.3.2 复杂数组类型

处理复杂数据结构的数组

在实际开发中,数组经常包含复杂的数据结构,TypeScript能够为这些复杂场景提供完整的类型支持:

  • 对象数组:最常见的复杂数组类型
  • 嵌套数组:多维数组的类型定义
  • 函数数组:存储函数的数组类型安全
  • 接口约束:使用接口定义数组元素结构
// 对象数组
interface User {
    id: number;
    name: string;
}

let users: User[] = [
    { id: 1, name: "张三" },
    { id: 2, name: "李四" }
];

// 嵌套数组
let matrix: number[][] = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
];

// 函数数组
type HandlerFunction = (data: string) => void;
let handlers: HandlerFunction[] = [
    (data) => console.log(`处理1: ${data}`),
    (data) => console.log(`处理2: ${data}`)
];
2.3.3 联合类型数组

支持多种类型的灵活数组

现实世界的数据往往不是单一类型,TypeScript的联合类型数组完美解决了这个问题:

  • 基础联合类型:(string | number)[]允许字符串和数字混合
  • 对象联合类型:使用discriminated union确保类型安全
  • 条件类型检查:运行时判断具体类型
  • 类型守卫应用:安全地访问联合类型的属性
// 混合类型数组使用联合类型
let mixedArray: (string | number)[] = ["TypeScript", 2024, "学习"];

// 更复杂的联合类型数组
type Status = "pending" | "success" | "error";
let statusList: Status[] = ["pending", "success", "error"];

// 对象联合类型数组
interface Dog {
    type: "dog";
    breed: string;
}

interface Cat {
    type: "cat";
    color: string;
}

let pets: (Dog | Cat)[] = [
    { type: "dog", breed: "金毛" },
    { type: "cat", color: "橘色" }
];
2.3.4 只读数组和元组

不可变数据结构的类型安全

函数式编程和不可变数据结构越来越重要,TypeScript提供了强大的只读数组和元组支持:

  • 只读数组:防止意外修改数组内容
  • 元组类型:固定长度和类型的数组,适合返回多个值
  • 可选元组元素:灵活的元组定义
  • 剩余元素:处理可变长度的结构化数据
  • 不可变性保证:编译时防止修改操作
// 只读数组
let readonlyFruits: readonly string[] = ["苹果", "香蕉"];
// readonlyFruits.push("橙子"); // 错误:只读数组不能修改
// readonlyFruits[0] = "葡萄"; // 错误:不能修改元素

// ReadonlyArray<T> 类型
let readonlyNumbers: ReadonlyArray<number> = [1, 2, 3, 4, 5];

// 元组类型(固定长度和类型的数组)
let coordinates: [number, number] = [10, 20];
let userInfo: [string, number, boolean] = ["张三", 25, true];

// 可选元组元素
let optionalTuple: [string, number?] = ["hello"]; // 第二个元素可选

// 剩余元素元组
let restTuple: [string, ...number[]] = ["标签", 1, 2, 3, 4];

// 只读元组
let readonlyTuple: readonly [string, number] = ["坐标", 100];
2.3.5 数组方法的类型安全

原生数组方法的完整类型支持

TypeScript为所有原生数组方法提供了精确的类型定义,确保链式调用的类型安全:

  • map方法:转换元素类型,返回新类型的数组
  • filter方法:过滤元素,保持原类型但可能返回undefined
  • reduce方法:复杂的累积操作类型推断
  • 链式调用:多个方法组合时的类型流转
  • 类型收窄:在回调函数中的类型细化
let numbers: number[] = [1, 2, 3, 4, 5];

// TypeScript能确保数组方法的类型安全
let doubled = numbers.map(n => n * 2); // 返回类型为number[]
let filtered = numbers.filter(n => n > 2); // 返回类型为number[]
let found = numbers.find(n => n > 3); // 返回类型为number | undefined

// 复杂的链式调用
let result = numbers
    .filter(n => n % 2 === 0)  // number[]
    .map(n => n.toString())    // string[]
    .join(", ");               // string

// reduce方法的类型推断
let sum = numbers.reduce((acc, curr) => acc + curr, 0); // number
let concatenated = numbers.reduce((acc, curr) => acc + curr.toString(), ""); // string

2.4 any、unknown、never类型详解

处理动态和特殊情况的类型

在TypeScript的类型系统中,有三个特殊的类型用于处理动态内容和边界情况:any、unknown和never。理解它们的区别和使用场景对于编写安全的TypeScript代码至关重要。

2.4.1 any类型的使用场景

灵活性与安全性的权衡

any类型是TypeScript提供的"逃生舱",它关闭了类型检查,应该谨慎使用:

  • 合理使用场景:第三方库迁移、动态内容处理、临时解决方案
  • 潜在危险:失去类型安全、智能提示缺失、运行时错误
  • 最佳实践:尽量避免使用,考虑unknown作为替代
  • 逐步迁移:在JavaScript项目向TypeScript迁移时的临时方案
// any类型的合理使用场景
function parseApiResponse(response: any): void {
    // 来自第三方API的数据,类型未知
    console.log(response.data);
    console.log(response.status);
}

// 逐步迁移JavaScript代码时的临时方案
let legacyData: any = getLegacyData();

// 动态内容(真正类型无法确定)
function handleDynamicContent(content: any): void {
    // 处理用户上传的各种格式文件
    if (typeof content === 'string') {
        console.log(content.length);
    } else if (Array.isArray(content)) {
        console.log(content.length);
    }
}

// any类型的危险性
let dangerousAny: any = "hello";
dangerousAny.toUpperCase(); // 运行时正常
dangerousAny = 42;
dangerousAny.toUpperCase(); // 运行时错误!TypeScript不会警告
2.4.2 unknown类型:安全的any

类型安全的顶级类型

unknown是TypeScript 3.0引入的安全版any,它保持了灵活性的同时强制进行类型检查:

  • 安全的顶级类型:任何值都可以赋给unknown,但使用前必须检查类型
  • 强制类型检查:防止直接访问未知类型的属性和方法
  • 类型收窄:通过类型守卫安全地使用unknown值
  • API设计:适合用于接收外部数据的函数参数类型
// unknown类型更安全
let safeUnknown: unknown = "hello";

// 必须进行类型检查才能使用
if (typeof safeUnknown === "string") {
    console.log(safeUnknown.toUpperCase()); // 类型安全
}

// unknown类型的实际应用
function parseJson(jsonString: string): unknown {
    return JSON.parse(jsonString);
}

const result = parseJson('{"name": "张三", "age": 25}');

// 需要类型断言或类型守卫
if (typeof result === "object" && result !== null && "name" in result) {
    console.log((result as { name: string }).name);
}

// 更好的做法:使用类型守卫
function isUser(obj: unknown): obj is { name: string; age: number } {
    return typeof obj === "object" && 
           obj !== null && 
           "name" in obj && 
           "age" in obj &&
           typeof (obj as any).name === "string" &&
           typeof (obj as any).age === "number";
}

if (isUser(result)) {
    console.log(result.name); // 类型安全
    console.log(result.age);
}
2.4.3 never类型:永不存在的值

表示永远不会发生的类型

never类型是TypeScript类型系统中的底部类型,表示永远不会出现的值。它在类型安全和详尽性检查中发挥着重要作用:

  • 抛出异常的函数:永远不会正常返回的函数
  • 无限循环:永远不会结束的函数
  • 详尽性检查:确保switch语句处理了所有可能的情况
  • 类型收窄:在条件语句中排除不可能的情况
  • 联合类型过滤:从联合类型中移除某些类型
// never类型表示永不会发生的类型
function throwError(message: string): never {
    throw new Error(message);
    // 这个函数永远不会正常返回
}

function infiniteLoop(): never {
    while (true) {
        console.log("永远循环");
    }
    // 这个函数永远不会结束
}

// never在联合类型中的应用
type Status = "loading" | "success" | "error";

function handleStatus(status: Status): string {
    switch (status) {
        case "loading":
            return "加载中...";
        case "success":
            return "成功";
        case "error":
            return "错误";
        default:
            // 这里status的类型是never
            // 确保我们处理了所有可能的情况
            const exhaustiveCheck: never = status;
            return exhaustiveCheck;
    }
}

// 利用never进行详尽性检查
type Shape = "circle" | "square" | "triangle";

function getArea(shape: Shape): number {
    switch (shape) {
        case "circle":
            return Math.PI * 5 * 5;
        case "square":
            return 5 * 5;
        // 如果我们忘记处理triangle,TypeScript会报错
        // case "triangle":
        //     return 0.5 * 5 * 5;
        default:
            const _exhaustiveCheck: never = shape; // 类型错误!
            return _exhaustiveCheck;
    }
}
2.4.4 void类型和undefined

表示无返回值和未定义值的区别

void和undefined都与"没有值"相关,但它们在TypeScript中有着不同的语义和用途:

  • void类型:表示函数没有返回值,常用于事件处理器和副作用函数
  • undefined类型:表示变量可能没有被赋值,或者函数显式返回undefined
  • 使用场景:void用于函数返回类型,undefined用于变量类型
  • 赋值规则:void函数可以返回undefined,但undefined函数必须显式返回
  • 类型兼容性:在非严格模式下,undefined可以赋值给void
// void类型:表示没有返回值
function logMessage(message: string): void {
    console.log(message);
    // 可以不显式返回任何值
}

function logMessageExplicit(message: string): void {
    console.log(message);
    return; // 可以显式返回undefined
}

// void与undefined的区别
function returnsVoid(): void {
    console.log("无返回值");
}

function returnsUndefined(): undefined {
    console.log("返回undefined");
    return undefined; // 必须显式返回undefined
}

// void类型的实际应用
type EventHandler = (event: Event) => void;

const button = document.createElement("button");
button.addEventListener("click", (event): void => {
    console.log("按钮被点击");
    // 不需要返回值
});
2.4.5 object、Object和{}的区别

三种对象类型的细微差别

TypeScript中有三种看似相似但实际不同的对象类型,理解它们的区别对于编写正确的类型定义很重要:

  • object类型:表示所有非原始类型,是最严格的对象类型定义
  • Object类型:表示JavaScript的Object类型,所有值都可以赋给它
  • {}类型:表示空对象类型,但实际上几乎所有值都可以赋给它
  • 实际使用:推荐使用具体的接口或类型别名而不是这些通用类型
  • 类型安全性:object > {} > Object(按安全性递减)
// object类型:表示非原始类型
let obj1: object = { name: "张三" };
let obj2: object = [1, 2, 3];
let obj3: object = new Date();
// let obj4: object = "string"; // 错误:string是原始类型

// Object类型:所有值都可以赋值给Object
let obj5: Object = "string"; // 可以
let obj6: Object = 42; // 可以
let obj7: Object = true; // 可以

// {}类型:空对象类型,也是所有值都可以赋值
let obj8: {} = "string"; // 可以
let obj9: {} = 42; // 可以
// let obj10: {} = null; // 错误(在严格模式下)

// 推荐的做法
interface User {
    name: string;
    age: number;
}

let user: User = { name: "张三", age: 25 }; // 明确的接口类型

3. 函数类型的艺术

什么是函数类型?

函数就像是一个"加工厂",你给它一些原料(参数),它给你加工出产品(返回值)。TypeScript的函数类型就是用来描述这个加工厂的:它需要什么原料,会产出什么产品。

3.1 函数参数和返回值类型

什么是函数签名?

函数签名就像是加工厂门口的说明牌,告诉你:

  • 这个厂需要什么原料(参数类型)
  • 会给你什么产品(返回值类型)

这样你就知道怎么使用这个加工厂了。

3.1.1 基础函数类型注解

怎么给函数标注类型?

给函数标注类型就像给加工厂写说明书:

  • 参数类型注解:告诉别人每个参数应该是什么类型,比如name: string
  • 返回值类型注解:告诉别人函数会返回什么类型,比如: string
  • 类型推断:TypeScript很聪明,有时候能自己猜出返回类型,但写明白更好
  • void类型:如果函数不返回任何东西,就用void
// 基础函数类型注解
function greetUser(name: string, greeting: string = "你好"): string {
    return `${greeting}, ${name}!`;
}

// 箭头函数的类型注解
const calculateArea = (width: number, height: number): number => {
    return width * height;
};

// 函数表达式
const multiply = function(a: number, b: number): number {
    return a * b;
};
3.1.2 参数类型详解

灵活而严格的参数类型系统

TypeScript提供了丰富的参数类型选项,让函数能够适应不同的调用场景:

  • 必需参数:调用时必须提供的参数
  • 可选参数:使用?修饰符,可以不传递的参数
  • 默认参数:有默认值的参数,类型会从默认值推断
  • 剩余参数:使用...收集多个参数到数组中
  • 参数顺序:可选参数必须在必需参数之后
// 必需参数
function processOrder(orderId: string, customerId: number): void {
    console.log(`处理订单 ${orderId},客户 ${customerId}`);
}

// 可选参数(必须在必需参数之后)
function createUser(name: string, email?: string, age?: number): object {
    const user: any = { name };
    if (email) user.email = email;
    if (age) user.age = age;
    return user;
}

// 默认参数
function formatMessage(message: string, prefix: string = "[INFO]"): string {
    return `${prefix} ${message}`;
}

// 剩余参数
function sum(...numbers: number[]): number {
    return numbers.reduce((total, num) => total + num, 0);
}

console.log(sum(1, 2, 3, 4, 5)); // 15

// 剩余参数的复杂用法
function logMessages(prefix: string, ...messages: string[]): void {
    messages.forEach(msg => console.log(`${prefix}: ${msg}`));
}

logMessages("DEBUG", "消息1", "消息2", "消息3");
3.1.3 函数重载

一个函数名,多种调用方式

函数重载允许同一个函数根据不同的参数类型和数量有不同的行为,这在处理多种输入格式时非常有用:

  • 重载声明:定义函数的多个签名
  • 实现签名:实际的函数实现,必须兼容所有重载
  • 类型选择:TypeScript根据参数自动选择匹配的重载
  • 返回类型精确:每个重载可以有精确的返回类型
  • 实际应用:API函数、工具函数的多态处理
// 函数重载声明
function processInput(input: string): string;
function processInput(input: number): number;
function processInput(input: boolean): boolean;

// 函数实现(必须兼容所有重载签名)
function processInput(input: string | number | boolean): string | number | boolean {
    if (typeof input === "string") {
        return input.toUpperCase();
    }
    if (typeof input === "number") {
        return input * 2;
    }
    return !input;
}

// 使用时TypeScript会根据参数类型选择正确的重载
let result1 = processInput("hello"); // string
let result2 = processInput(42); // number
let result3 = processInput(true); // boolean

// 更复杂的函数重载示例
interface User {
    id: number;
    name: string;
}

function findUser(id: number): User | undefined;
function findUser(name: string): User[] | undefined;
function findUser(criteria: number | string): User | User[] | undefined {
    if (typeof criteria === "number") {
        // 根据ID查找单个用户
        return { id: criteria, name: "测试用户" };
    } else {
        // 根据名称查找多个用户
        return [{ id: 1, name: criteria }];
    }
}

3.2 函数类型表达式和高阶函数

函数作为值的类型定义

在JavaScript中,函数是一等公民,可以作为值传递、存储和操作。TypeScript为这种函数式编程模式提供了完整的类型支持。

3.2.1 函数类型别名

复用函数签名的优雅方式

函数类型别名让我们能够为函数签名创建可复用的类型定义:

  • 类型别名:使用type关键字定义函数类型
  • 签名复用:相同的函数签名可以在多处使用
  • 语义化:给函数类型起有意义的名字
  • 维护性:修改函数签名时只需要在一个地方修改
// 函数类型别名
type CalculatorFunction = (a: number, b: number) => number;

const add: CalculatorFunction = (x, y) => x + y;
const subtract: CalculatorFunction = (x, y) => x - y;
const multiply: CalculatorFunction = (x, y) => x * y;

// 使用函数类型别名
function calculate(op: CalculatorFunction, a: number, b: number): number {
    return op(a, b);
}

console.log(calculate(add, 10, 5)); // 15
console.log(calculate(multiply, 3, 4)); // 12
3.2.2 复杂函数类型

处理异步和回调的类型定义

现代JavaScript大量使用异步操作和回调函数,TypeScript为这些复杂场景提供了强大的类型支持:

  • 回调函数类型:定义回调函数的参数和返回值
  • 泛型函数类型:使用泛型创建灵活的函数类型
  • 异步操作类型:处理Promise、回调等异步模式
  • 错误处理:在类型层面处理成功和失败情况
// 回调函数类型
type EventCallback<T> = (data: T) => void;
type ErrorCallback = (error: Error) => void;

// 异步操作函数类型
type AsyncOperation<T> = (
    onSuccess: EventCallback<T>,
    onError: ErrorCallback
) => void;

// 使用复杂函数类型
const fetchUserData: AsyncOperation<User> = (onSuccess, onError) => {
    setTimeout(() => {
        try {
            const user = { id: 1, name: "张三" };
            onSuccess(user);
        } catch (error) {
            onError(error as Error);
        }
    }, 1000);
};

// 调用
fetchUserData(
    (user) => console.log("用户数据:", user),
    (error) => console.error("错误:", error.message)
);
3.2.3 高阶函数模式

函数式编程的类型安全实现

高阶函数是函数式编程的核心概念,TypeScript让这些模式变得类型安全:

  • 函数工厂:返回函数的函数,创建特定行为的函数
  • 函数组合:将多个函数组合成一个新函数
  • 柯里化:将多参数函数转换为单参数函数序列
  • 装饰器模式:为现有函数添加额外功能
  • 闭包类型安全:确保闭包中变量的类型安全
// 高阶函数:返回函数的函数
function createCounter(initialValue: number): () => number {
    let count = initialValue;
    return () => ++count;
}

const counter = createCounter(0);
console.log(counter()); // 1
console.log(counter()); // 2

// 更复杂的高阶函数:函数工厂
type ValidationRule<T> = (value: T) => boolean;

function createValidator<T>(rules: ValidationRule<T>[]): ValidationRule<T> {
    return (value: T) => {
        return rules.every(rule => rule(value));
    };
}

// 创建验证规则
const isNotEmpty: ValidationRule<string> = (value) => value.length > 0;
const isLongEnough: ValidationRule<string> = (value) => value.length >= 6;
const hasNumbers: ValidationRule<string> = (value) => /\d/.test(value);

// 组合验证器
const passwordValidator = createValidator([isNotEmpty, isLongEnough, hasNumbers]);

console.log(passwordValidator("123456")); // true
console.log(passwordValidator("abc")); // false

// 柯里化函数
type CurriedFunction<A, B, C> = (a: A) => (b: B) => C;

const curriedAdd: CurriedFunction<number, number, number> = (a) => (b) => a + b;

const add5 = curriedAdd(5);
console.log(add5(3)); // 8

// 函数组合
type UnaryFunction<T, R> = (arg: T) => R;

function compose<A, B, C>(
    f: UnaryFunction<B, C>,
    g: UnaryFunction<A, B>
): UnaryFunction<A, C> {
    return (x: A) => f(g(x));
}

const addOne = (x: number) => x + 1;
const double = (x: number) => x * 2;

const addOneThenDouble = compose(double, addOne);
console.log(addOneThenDouble(3)); // 8 (3 + 1 = 4, 4 * 2 = 8)
3.2.4 装饰器模式函数

为函数增强功能的类型安全方式

装饰器模式允许我们在不修改原函数的情况下为其添加额外功能,TypeScript确保这个过程是类型安全的:

  • 函数装饰器:包装原函数,添加额外行为
  • 性能监控:添加计时功能,监控函数执行时间
  • 缓存机制:为纯函数添加结果缓存
  • 日志记录:自动记录函数调用和结果
  • 类型保持:装饰后的函数保持原有的类型签名
// 函数装饰器
type DecoratedFunction<T extends (...args: any[]) => any> = T;

function timing<T extends (...args: any[]) => any>(fn: T): T {
    return ((...args: any[]) => {
        console.time('执行时间');
        const result = fn(...args);
        console.timeEnd('执行时间');
        return result;
    }) as T;
}

function cache<T extends (...args: any[]) => any>(fn: T): T {
    const cacheMap = new Map();
    
    return ((...args: any[]) => {
        const key = JSON.stringify(args);
        if (cacheMap.has(key)) {
            console.log('从缓存返回');
            return cacheMap.get(key);
        }
        
        const result = fn(...args);
        cacheMap.set(key, result);
        return result;
    }) as T;
}

// 使用装饰器
const expensiveCalculation = timing(cache((n: number): number => {
    // 模拟耗时计算
    let result = 0;
    for (let i = 0; i < n * 1000000; i++) {
        result += i;
    }
    return result;
}));

console.log(expensiveCalculation(100)); // 第一次计算,显示执行时间
console.log(expensiveCalculation(100)); // 从缓存返回,很快

3.3 上下文类型推断和this类型

智能的类型推断让代码更简洁

TypeScript的类型推断不仅仅基于变量的值,还会考虑使用的上下文环境,这让我们能够编写更少类型注解的代码。

3.3.1 上下文类型推断

根据使用环境自动推断类型

上下文类型推断是TypeScript最智能的特性之一,它能根据函数的使用环境自动推断参数类型:

  • 数组方法推断:map、filter、forEach等方法的回调函数参数类型自动推断
  • 事件处理器推断:DOM事件监听器的事件参数类型自动推断
  • Promise链推断:then方法的回调函数参数类型自动推断
  • 函数参数推断:高阶函数的回调参数类型自动推断
  • 减少冗余:避免重复的类型注解,让代码更简洁
// TypeScript可以从上下文推断函数参数类型
const userNames = ["Alice", "Bob", "Charlie"];

// 参数item会被推断为string类型
userNames.forEach((item) => {
    console.log(item.toUpperCase()); // TypeScript知道item是string
});

// map方法的上下文类型推断
const userAges = [25, 30, 35];
const ageDescriptions = userAges.map((age) => {
    return `${age}岁`; // age被推断为number类型
});

// 事件处理器的上下文类型推断
const button = document.querySelector('button');
button?.addEventListener('click', (event) => {
    // event参数自动推断为MouseEvent类型
    console.log(event.clientX, event.clientY);
});

// Promise的上下文类型推断
fetch('/api/users')
    .then((response) => {
        // response自动推断为Response类型
        return response.json();
    })
    .then((data) => {
        // data推断为any类型,因为json()返回Promise<any>
        console.log(data);
    });
3.3.2 this类型详解

TypeScript中this的类型安全处理

JavaScript中的this一直是令人困惑的概念,TypeScript通过类型系统让this的使用变得安全和可预测:

  • 显式this参数:在函数签名中明确this的类型
  • this返回类型:支持链式调用的类型安全
  • 方法绑定检查:确保方法在正确的上下文中调用
  • 类型收窄:在不同上下文中this类型的自动收窄
  • ThisType工具类型:高级this类型操作
// 函数中的this类型
interface Calculator {
    value: number;
    add(this: Calculator, num: number): Calculator;
    multiply(this: Calculator, num: number): Calculator;
    getValue(this: Calculator): number;
}

const calculator: Calculator = {
    value: 0,
    add(num: number) {
        this.value += num;
        return this; // 支持链式调用
    },
    multiply(num: number) {
        this.value *= num;
        return this;
    },
    getValue() {
        return this.value;
    }
};

// 链式调用
const result = calculator.add(5).multiply(2).getValue(); // 10

// this类型在类中的应用
class FluentAPI {
    private data: string[] = [];

    add(item: string): this {
        this.data.push(item);
        return this;
    }

    remove(item: string): this {
        const index = this.data.indexOf(item);
        if (index > -1) {
            this.data.splice(index, 1);
        }
        return this;
    }

    clear(): this {
        this.data = [];
        return this;
    }

    getAll(): string[] {
        return [...this.data];
    }
}

const api = new FluentAPI();
const items = api.add("item1").add("item2").remove("item1").getAll();

// ThisType工具类型
interface APIContext {
    request: (url: string) => Promise<any>;
    cache: Map<string, any>;
}

type APIModule = {
    getUser(): Promise<User>;
    getUsers(): Promise<User[]>;
} & ThisType<APIContext>;

const userAPI: APIModule = {
    async getUser() {
        // this被推断为APIContext类型
        return this.request('/api/user');
    },
    async getUsers() {
        const cached = this.cache.get('users');
        if (cached) return cached;
        
        const users = await this.request('/api/users');
        this.cache.set('users', users);
        return users;
    }
};

4. 对象类型的精细控制

对象是JavaScript应用的核心数据结构

在JavaScript应用中,对象是最重要的数据结构。TypeScript为对象提供了极其精细的类型控制能力,从简单的属性类型到复杂的结构约束,让我们能够构建类型安全的数据模型。

4.1 对象类型定义详解

多种方式定义对象的形状

TypeScript提供了多种定义对象类型的方式,每种方式都有其适用场景和优势。

4.1.1 内联对象类型

直接在使用处定义对象结构

内联对象类型是最直接的对象类型定义方式,适合一次性使用的场景:

  • 快速定义:在函数参数或变量声明处直接定义对象结构
  • 嵌套支持:支持深层嵌套的对象结构定义
  • 临时使用:适合不需要复用的一次性对象类型
  • 简单明了:类型定义和使用在同一位置,易于理解
  • 局限性:不能复用,复杂结构会降低可读性
// 内联对象类型
function printUserProfile(user: { name: string; age: number; email?: string }): void {
    console.log(`姓名:${user.name}`);
    console.log(`年龄:${user.age}`);
    if (user.email) {
        console.log(`邮箱:${user.email}`);
    }
}

// 嵌套对象类型
function processOrder(order: {
    id: string;
    customer: {
        name: string;
        address: {
            street: string;
            city: string;
            zipCode: string;
        };
    };
    items: Array<{
        productId: string;
        quantity: number;
        price: number;
    }>;
}): void {
    console.log(`订单 ${order.id} 客户:${order.customer.name}`);
    console.log(`收货地址:${order.customer.address.city}`);
}
4.1.2 类型别名和结构化

构建可复用的类型系统

类型别名是构建大型应用类型系统的基础,它让我们能够创建清晰、可维护的类型定义:

  • 类型复用:定义一次,多处使用,提高代码复用性
  • 语义化命名:给复杂类型起有意义的名字,提高可读性
  • 模块化设计:将大型对象类型分解为小的可组合单元
  • 维护性:修改类型定义时只需要在一个地方修改
  • 组合能力:不同类型可以组合成更复杂的类型结构
// 类型别名提高可读性
type UserProfile = {
    name: string;
    age: number;
    email?: string;
    readonly id: string;
};

// 地址类型
type Address = {
    street: string;
    city: string;
    zipCode: string;
    country?: string;
};

// 客户类型
type Customer = {
    id: string;
    profile: UserProfile;
    address: Address;
    membershipLevel: "bronze" | "silver" | "gold" | "platinum";
};

// 订单项类型
type OrderItem = {
    productId: string;
    productName: string;
    quantity: number;
    unitPrice: number;
    discount?: number;
};

// 订单类型
type Order = {
    readonly id: string;
    customer: Customer;
    items: OrderItem[];
    status: "pending" | "processing" | "shipped" | "delivered" | "cancelled";
    createdAt: Date;
    updatedAt: Date;
};

function createUserProfile(data: Omit<UserProfile, 'id'>): UserProfile {
    return {
        ...data,
        id: Math.random().toString(36).substr(2, 9)
    };
}

// 计算订单总价的复杂示例
function calculateOrderTotal(order: Order): number {
    return order.items.reduce((total, item) => {
        const itemTotal = item.quantity * item.unitPrice;
        const discountAmount = item.discount ? itemTotal * (item.discount / 100) : 0;
        return total + (itemTotal - discountAmount);
    }, 0);
}
4.1.3 只读属性和可选属性

精细控制对象属性的可变性

TypeScript允许我们精确控制对象属性的可变性和可选性,这对于构建安全的数据模型非常重要:

  • 只读属性:使用readonly修饰符防止属性被修改
  • 可选属性:使用?修饰符表示属性可能不存在
  • 不可变设计:构建不可变的数据结构,提高应用的可预测性
  • 部分更新模式:结合可选属性实现灵活的数据更新
  • 深度只读:递归地将所有属性设置为只读
// 只读属性的深入应用
type ImmutableUser = {
    readonly id: string;
    readonly createdAt: Date;
    readonly profile: {
        readonly name: string;
        readonly email: string;
    };
    // 可修改的属性
    lastLoginAt?: Date;
    isActive: boolean;
};

// 深度只读类型
type DeepReadonly<T> = {
    readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};

type CompletelyImmutableUser = DeepReadonly<Customer>;

// 可选属性的高级用法
type PartialUpdate<T> = {
    [P in keyof T]?: T[P] extends object ? PartialUpdate<T[P]> : T[P];
};

function updateUser(id: string, updates: PartialUpdate<UserProfile>): UserProfile {
    // 实现用户更新逻辑
    const currentUser = getUserById(id); // 假设的函数
    return {
        ...currentUser,
        ...updates
    };
}

// 示例:只更新部分字段
const updatedUser = updateUser("123", {
    age: 26, // 只更新年龄
    // name 和 email 保持不变
});

4.2 接口vs类型别名深度对比

两种定义对象类型的方式,各有所长

TypeScript提供了接口(interface)和类型别名(type alias)两种定义对象类型的方式。虽然它们在很多场景下可以互换使用,但理解它们的区别和适用场景对于写出优雅的TypeScript代码很重要。

4.2.1 接口的特性

面向对象编程的经典概念

接口是从面向对象编程中借鉴的概念,在TypeScript中有着独特的特性:

  • 继承机制:支持extends关键字进行继承,可以建立类型层次结构
  • 多重继承:一个接口可以继承多个其他接口
  • 声明合并:同名接口会自动合并,这在库的类型扩展中很有用
  • 类实现:类可以使用implements关键字实现接口
  • 开放扩展:便于在不同模块中对同一接口进行扩展
// 接口定义
interface Vehicle {
    brand: string;
    model: string;
    year: number;
}

// 接口可以扩展
interface Car extends Vehicle {
    doors: number;
    fuelType: "gasoline" | "electric" | "hybrid";
}

// 多重继承
interface ElectricVehicle {
    batteryCapacity: number;
    chargingTime: number;
}

interface Autonomous {
    autopilotLevel: 1 | 2 | 3 | 4 | 5;
}

interface ElectricCar extends Car, ElectricVehicle, Autonomous {
    fastChargingSupported: boolean;
}

// 接口可以重新声明(声明合并)
interface Vehicle {
    color?: string; // 这会合并到上面的Vehicle接口中
}

// 现在Vehicle接口包含了所有属性
const myVehicle: Vehicle = {
    brand: "Tesla",
    model: "Model 3",
    year: 2023,
    color: "red"
};
4.2.2 类型别名的特性

更强大的类型计算能力

类型别名提供了比接口更强大的类型表达能力,特别适合复杂的类型操作:

  • 联合类型:可以表示多种可能的类型选择
  • 交集类型:使用&操作符组合多个类型
  • 条件类型:根据条件选择不同的类型
  • 映射类型:基于现有类型生成新类型
  • 模板字面量类型:基于字符串模式生成类型
// 类型别名的交集类型
type Motorcycle = Vehicle & {
    engineSize: number;
    hasWindshield: boolean;
};

// 类型别名可以表示联合类型
type Status = "loading" | "success" | "error";
type ID = string | number;

// 类型别名可以表示条件类型
type NonNullable<T> = T extends null | undefined ? never : T;

// 类型别名可以表示映射类型
type Partial<T> = {
    [P in keyof T]?: T[P];
};

// 复杂的类型操作
type Keys<T> = keyof T;
type Values<T> = T[keyof T];

type VehicleKeys = Keys<Vehicle>; // "brand" | "model" | "year" | "color"
type VehicleValues = Values<Vehicle>; // string | number | undefined
4.2.3 何时使用接口 vs 类型别名

选择的指导原则和最佳实践

选择接口还是类型别名不是随意的,有明确的指导原则:

  • 对象形状定义:优先使用接口,特别是会被类实现的场景
  • 联合和交集类型:必须使用类型别名
  • 需要扩展的场景:接口的声明合并特性更适合
  • 复杂类型计算:类型别名的表达能力更强
  • 库的API设计:接口提供更好的扩展性
// 推荐使用接口的场景:

// 1. 定义对象的结构(特别是会被类实现的)
interface Drawable {
    draw(): void;
    getArea(): number;
}

class Circle implements Drawable {
    constructor(private radius: number) {}
    
    draw(): void {
        console.log(`绘制半径为 ${this.radius} 的圆`);
    }
    
    getArea(): number {
        return Math.PI * this.radius ** 2;
    }
}

// 2. 需要声明合并的场景
interface Window {
    customProperty: string;
}

// 在另一个文件中可以扩展
interface Window {
    anotherCustomProperty: number;
}

// 推荐使用类型别名的场景:

// 1. 联合类型
type Theme = "light" | "dark" | "auto";
type EventType = "click" | "hover" | "focus";

// 2. 交集类型
type Timestamped = {
    createdAt: Date;
    updatedAt: Date;
};

type User = {
    id: string;
    name: string;
};

type TimestampedUser = User & Timestamped;

// 3. 条件类型和映射类型
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

type UserWithOptionalEmail = Optional<User, 'email'>;

// 4. 计算类型
type EventMap = {
    click: MouseEvent;
    keydown: KeyboardEvent;
    load: Event;
};

type EventListener<T extends keyof EventMap> = (event: EventMap[T]) => void;

const clickHandler: EventListener<'click'> = (event) => {
    console.log(event.clientX, event.clientY); // 类型安全
};
4.2.4 接口和类型别名的互操作性
// 接口可以扩展类型别名
type Point = {
    x: number;
    y: number;
};

interface ColoredPoint extends Point {
    color: string;
}

// 类型别名可以与接口进行交集
interface Shape {
    area: number;
}

type ColoredShape = Shape & {
    color: string;
};

// 混合使用的实际示例
interface APIResponse {
    status: number;
    message: string;
}

type SuccessResponse<T> = APIResponse & {
    status: 200;
    data: T;
};

type ErrorResponse = APIResponse & {
    status: 400 | 404 | 500;
    error: string;
};

type Response<T> = SuccessResponse<T> | ErrorResponse;

// 使用示例
function handleUserResponse(response: Response<User>): void {
    if (response.status === 200) {
        // TypeScript知道这是SuccessResponse<User>
        console.log("用户数据:", response.data.name);
    } else {
        // TypeScript知道这是ErrorResponse
        console.error("错误:", response.error);
    }
}

4.3 索引签名的使用

// 字符串索引签名
interface StringDictionary {
    [key: string]: string;
}

const translations: StringDictionary = {
    hello: "你好",
    goodbye: "再见",
    thanks: "谢谢"
};

// 数字索引签名
interface NumberArray {
    [index: number]: number;
    length: number; // 显式属性必须兼容索引签名
}

const fibonacci: NumberArray = {
    0: 1,
    1: 1,
    2: 2,
    3: 3,
    4: 5,
    length: 5
};

5. 联合类型与类型缩小

什么是联合类型?

联合类型就像是一个"多功能盒子",可以装不同类型的东西。比如一个变量可能是字符串,也可能是数字,联合类型就能描述这种"要么是这个,要么是那个"的情况。

5.1 联合类型的强大功能

怎么用联合类型?

联合类型用|符号连接,就像说"或者":

  • 类型组合:用|把不同类型连起来,比如string | number表示"要么是字符串,要么是数字"
  • 精确控制:只能是你指定的几种类型,其他的不行
  • 代码表达性:让类型定义更符合实际业务需求
  • 错误预防:如果你传了不对的类型,TypeScript会提醒你
  • 智能提示:IDE知道可能的类型,会给你更准确的提示
// 联合类型定义
type Status = "loading" | "success" | "error";
type Id = string | number;

function handleApiResponse(status: Status, data?: unknown): void {
    switch (status) {
        case "loading":
            console.log("正在加载...");
            break;
        case "success":
            console.log("加载成功!", data);
            break;
        case "error":
            console.log("加载失败!");
            break;
    }
}

// 联合类型在函数参数中的应用
function formatId(id: Id): string {
    if (typeof id === "string") {
        return `ID: ${id.toUpperCase()}`;
    }
    return `ID: ${id.toString().padStart(6, '0')}`;
}

5.2 类型缩小技术

什么是类型缩小?

类型缩小就像是"开盲盒",你知道盒子里可能有几种东西,但要打开看看才知道具体是什么。TypeScript提供了几种"开盒子"的方法:

  • typeof检查:用typeof看看是不是基本类型,比如字符串、数字
  • instanceof检查:看看对象是不是某个类的实例
  • in操作符:看看对象里有没有某个属性
  • 自定义类型守卫:自己写函数来判断类型
  • 控制流分析:TypeScript会自动分析你的if-else语句,知道每个分支里变量是什么类型
// 使用typeof进行类型缩小
function processValue(value: string | number | boolean): string {
    if (typeof value === "string") {
        // 在这个分支中,value确定是string类型
        return value.trim().toLowerCase();
    }
    
    if (typeof value === "number") {
        // 在这个分支中,value确定是number类型
        return value.toFixed(2);
    }
    
    // 在这个分支中,value确定是boolean类型
    return value ? "true" : "false";
}

// 使用instanceof进行类型缩小
function handleError(error: Error | string): void {
    if (error instanceof Error) {
        // error是Error类型
        console.error(error.message);
        console.error(error.stack);
    } else {
        // error是string类型
        console.error(error);
    }
}

// 使用in操作符进行类型缩小
interface Bird {
    fly(): void;
    layEggs(): void;
}

interface Fish {
    swim(): void;
    layEggs(): void;
}

function moveAnimal(animal: Bird | Fish): void {
    if ("fly" in animal) {
        // animal是Bird类型
        animal.fly();
    } else {
        // animal是Fish类型
        animal.swim();
    }
    
    // 两种类型都有layEggs方法
    animal.layEggs();
}

6. 字面量类型与模板字面量类型

什么是字面量类型?

字面量类型就像是"定制盒子",不仅要求类型对,还要求值也必须是特定的。比如不仅要求是字符串,还必须是"red"、"green"、"blue"中的一个。

6.1 字面量类型的精确控制

字面量类型有什么用?

字面量类型让你的代码更精确,就像点菜时不只说"要肉",而是说"要牛肉":

  • 字符串字面量:比如颜色只能是"red" | "green" | "blue"
  • 数字字面量:比如骰子只能是1 | 2 | 3 | 4 | 5 | 6
  • 布尔字面量:比如开关状态只能是true或false
  • 配置约束:用于限制配置选项,防止写错
  • API安全性:确保调用API时参数值是正确的
// 字符串字面量类型
type Theme = "light" | "dark" | "auto";
type Size = "small" | "medium" | "large";

function setTheme(theme: Theme): void {
    document.body.className = `theme-${theme}`;
}

// 数字字面量类型
type DiceValue = 1 | 2 | 3 | 4 | 5 | 6;

function rollDice(): DiceValue {
    return Math.ceil(Math.random() * 6) as DiceValue;
}

// 布尔字面量类型
type LoadingState = true;
type ErrorState = false;

function createLoadingIndicator(isLoading: LoadingState): void {
    // isLoading只能是true
    console.log("显示加载指示器");
}

6.2 模板字面量类型的强大表达

编译时的字符串模式匹配和生成

模板字面量类型是TypeScript 4.1引入的强大特性,它将模板字符串的概念引入到类型系统中:

  • 字符串模式:使用模板语法定义字符串类型模式
  • 类型插值:在模板中插入其他类型
  • 大小写转换:内置的Capitalize、Lowercase等工具类型
  • API类型安全:为REST API路径和方法组合生成精确类型
  • 代码生成:基于模式自动生成大量相关类型
// 模板字面量类型
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<"click">; // "onClick"
type ChangeEvent = EventName<"change">; // "onChange"

// 复杂的模板字面量类型
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type ApiPath = "/users" | "/products" | "/orders";
type ApiEndpoint = `${HttpMethod} ${ApiPath}`;

// "GET /users" | "POST /users" | "PUT /users" | "DELETE /users" | ...

function callApi(endpoint: ApiEndpoint): void {
    const [method, path] = endpoint.split(" ");
    console.log(`调用API: ${method} ${path}`);
}

callApi("GET /users"); // ✅ 正确
// callApi("PATCH /users"); // ❌ 错误:不在联合类型中

7. 类型断言与非空断言

什么是类型断言?

类型断言就像是告诉TypeScript:"相信我,我知道这个变量是什么类型"。就像你对朋友说"相信我,这个盒子里装的是苹果",即使盒子上没有标签。

7.1 类型断言的使用场景

什么时候需要类型断言?

类型断言就像是"强制说服",在以下情况下会用到:

  • DOM操作:比如你知道某个元素是按钮,但TypeScript只知道它是普通元素
  • API响应:从服务器返回的数据TypeScript不知道是什么,但你知道
  • 类型收窄:在复杂情况下,手动告诉TypeScript某个值的具体类型
  • 第三方库:有些库的类型定义不完整,需要你自己断言
  • 双重断言:极端情况下的强制转换(很危险,尽量不用)
// 类型断言在DOM操作中的应用
const canvas = document.getElementById("myCanvas") as HTMLCanvasElement;
const context = canvas.getContext("2d");

// 角括号语法(在JSX中不可用)
const input = <HTMLInputElement>document.getElementById("myInput");

// 双重断言(谨慎使用)
const value = "hello" as unknown as number; // 强制类型转换

// 更安全的类型断言
function isString(value: unknown): value is string {
    return typeof value === "string";
}

function processUnknownValue(value: unknown): void {
    if (isString(value)) {
        // 类型守卫确保value是string类型
        console.log(value.toUpperCase());
    }
}

7.2 非空断言操作符

确信某个值不是null或undefined

非空断言操作符(!)是一种特殊的类型断言,专门用于处理可能为null或undefined的值:

  • 确定性断言:当你确定某个值不会是null或undefined时使用
  • 简化代码:避免繁琐的null检查
  • DOM查询:处理DOM查询可能返回null的情况
  • 可选属性:访问可选属性时的快捷方式
  • 谨慎使用:滥用会导致运行时错误,推荐使用可选链
// 非空断言操作符的使用
interface User {
    name: string;
    email?: string;
}

function sendEmail(user: User): void {
    // 当我们确定email一定存在时,可以使用非空断言
    const emailAddress = user.email!; // 告诉TypeScript这里不会是undefined
    console.log(`发送邮件到: ${emailAddress.toLowerCase()}`);
}

// 更安全的做法是使用可选链
function sendEmailSafely(user: User): void {
    if (user.email) {
        console.log(`发送邮件到: ${user.email.toLowerCase()}`);
    } else {
        console.log("用户没有邮箱地址");
    }
}

// 或者使用可选链操作符
function sendEmailWithOptionalChaining(user: User): void {
    console.log(`发送邮件到: ${user.email?.toLowerCase() ?? "无邮箱"}`);
}

8. null和undefined的处理

什么是null和undefined?

null和undefined就像是"空盒子",是JavaScript中最容易出错的地方。就像你以为盒子里有东西,结果打开一看是空的,程序就崩溃了。TypeScript帮你提前检查盒子是不是空的。

8.1 严格空检查的重要性

什么是严格空检查?

严格空检查就像是一个"空盒子检测器",在你打开盒子之前就告诉你里面是不是空的:

  • 编译时检查:在代码运行之前就发现"空盒子"问题
  • 明确的可空类型:明确告诉你哪些变量可能是空的
  • 强制检查:如果可能是空的,必须先检查一下再用
  • 减少运行时错误:大大减少"找不到属性"这种错误
  • 代码可靠性:让你的代码更稳定,不容易崩溃
// 开启strictNullChecks后的代码
function findUser(id: string): User | null {
    // 模拟数据库查询
    const users = [
        { id: "1", name: "Alice" },
        { id: "2", name: "Bob" }
    ];
    
    return users.find(user => user.id === id) || null;
}

// 安全地处理可能为null的值
function displayUserName(userId: string): void {
    const user = findUser(userId);
    
    if (user !== null) {
        console.log(`用户名: ${user.name}`);
    } else {
        console.log("用户不存在");
    }
}

// 使用可选链简化代码
function displayUserNameOptional(userId: string): void {
    const user = findUser(userId);
    console.log(`用户名: ${user?.name ?? "未知用户"}`);
}

8.2 空值合并操作符

更精确的默认值处理机制

空值合并操作符(??)是ES2020引入的新特性,TypeScript完全支持并为其提供了类型安全保障:

  • 精确的空值判断:只有null和undefined才触发默认值
  • 区别于逻辑或:不会将0、""、false等视为空值
  • 配置对象:在处理配置对象时特别有用
  • 链式调用:可以与可选链操作符组合使用
  • 类型推断:TypeScript能正确推断结果类型
// 空值合并操作符的实用场景
interface AppConfig {
    apiUrl?: string;
    timeout?: number;
    retries?: number;
}

function createApiClient(config: AppConfig) {
    // 使用??提供默认值,只有null或undefined时才使用默认值
    const apiUrl = config.apiUrl ?? "https://api.example.com";
    const timeout = config.timeout ?? 5000;
    const retries = config.retries ?? 3;
    
    return {
        apiUrl,
        timeout,
        retries,
        request: (endpoint: string) => {
            console.log(`请求: ${apiUrl}${endpoint}, 超时: ${timeout}ms`);
        }
    };
}

// 注意??与||的区别
const value1 = 0 || "default"; // "default" (0被视为falsy)
const value2 = 0 ?? "default"; // 0 (只有null/undefined才使用默认值)

9. 实战练习:构建类型安全的用户管理系统

综合运用所学知识的实战项目

通过构建一个完整的用户管理系统,我们将综合运用今天学到的所有TypeScript特性。这个项目展示了如何在实际开发中应用类型安全的设计模式。

项目特点:

  • 类型安全的数据模型:使用接口和联合类型定义用户结构
  • 泛型方法:构建可复用的类型安全方法
  • 权限系统:基于角色的访问控制
  • 错误处理:完善的空值检查和错误处理
  • 实用工具类型:使用Omit、Extract等工具类型
// 定义用户相关类型
interface BaseUser {
    readonly id: string;
    name: string;
    email: string;
    createdAt: Date;
}

interface AdminUser extends BaseUser {
    role: "admin";
    permissions: Permission[];
}

interface RegularUser extends BaseUser {
    role: "user";
    lastLoginAt?: Date;
}

type User = AdminUser | RegularUser;
type Permission = "read" | "write" | "delete" | "admin";

// 用户管理类
class UserManager {
    private users: Map<string, User> = new Map();

    createUser(userData: Omit<User, 'id' | 'createdAt'>): User {
        const id = this.generateId();
        const user: User = {
            ...userData,
            id,
            createdAt: new Date()
        };
        
        this.users.set(id, user);
        return user;
    }

    getUser(id: string): User | undefined {
        return this.users.get(id);
    }

    getUsersByRole<T extends User['role']>(role: T): Extract<User, { role: T }>[] {
        const result: Extract<User, { role: T }>[] = [];
        
        for (const user of this.users.values()) {
            if (user.role === role) {
                result.push(user as Extract<User, { role: T }>);
            }
        }
        
        return result;
    }

    hasPermission(userId: string, permission: Permission): boolean {
        const user = this.getUser(userId);
        
        if (!user) return false;
        
        if (user.role === "admin") {
            return user.permissions.includes(permission);
        }
        
        return false; // 普通用户没有特殊权限
    }

    private generateId(): string {
        return Math.random().toString(36).substr(2, 9);
    }
}

// 使用示例
const userManager = new UserManager();

// 创建管理员用户
const admin = userManager.createUser({
    name: "管理员",
    email: "admin@example.com",
    role: "admin",
    permissions: ["read", "write", "delete", "admin"]
});

// 创建普通用户
const regularUser = userManager.createUser({
    name: "普通用户",
    email: "user@example.com",
    role: "user"
});

// 类型安全的权限检查
console.log(userManager.hasPermission(admin.id, "admin")); // true
console.log(userManager.hasPermission(regularUser.id, "admin")); // false

// 获取特定角色的用户
const admins = userManager.getUsersByRole("admin");
const users = userManager.getUsersByRole("user");

🎯 总结与下一步

第一天学了什么?

经过今天的学习,我们掌握了TypeScript的基础知识,就像学会了基本的"积木搭建法"。

今天的核心收获:

今天我们学会了TypeScript的基本概念:

  1. 静态类型检查 - 就像有个助手帮你提前检查错误
  2. 基础类型系统 - 学会了给不同的数据贴标签(string、number、boolean等)
  3. 函数类型 - 学会了给"加工厂"写说明书
  4. 对象类型 - 学会了描述复杂的数据结构
  5. 联合类型 - 学会了处理"要么是这个,要么是那个"的情况
  6. 字面量类型 - 学会了更精确的类型控制
  7. 类型断言 - 学会了在必要时"强制说服"TypeScript
  8. 空值处理 - 学会了避免"空盒子"错误

下一步学习计划

接下来学什么?

掌握了今天的基础知识后,我们将继续学习更高级的"积木搭建技巧":

  • 第二天:泛型、条件类型、映射类型 - 学会制作"万能积木"和"智能积木"
  • 第三天:类与继承、装饰器、模块系统 - 学会搭建大型"积木建筑"
  • 第四天:高级类型操作、工具类型、类型编程 - 学会"积木魔法"
  • 第五天:现代JavaScript特性与TypeScript集成 - 学会将"积木"与现代工具完美融合

学习建议:

  • 多写代码,在实践中巩固理论知识
  • 启用strict模式,让TypeScript更严格地帮你检查错误
  • 看看优秀项目的TypeScript代码,学习别人怎么写
  • 试着把现有的JavaScript项目改成TypeScript

通过这五天的学习,你将从TypeScript新手变成能够熟练使用TypeScript的开发者!