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的基本概念:
- 静态类型检查 - 就像有个助手帮你提前检查错误
- 基础类型系统 - 学会了给不同的数据贴标签(string、number、boolean等)
- 函数类型 - 学会了给"加工厂"写说明书
- 对象类型 - 学会了描述复杂的数据结构
- 联合类型 - 学会了处理"要么是这个,要么是那个"的情况
- 字面量类型 - 学会了更精确的类型控制
- 类型断言 - 学会了在必要时"强制说服"TypeScript
- 空值处理 - 学会了避免"空盒子"错误
下一步学习计划
接下来学什么?
掌握了今天的基础知识后,我们将继续学习更高级的"积木搭建技巧":
- 第二天:泛型、条件类型、映射类型 - 学会制作"万能积木"和"智能积木"
- 第三天:类与继承、装饰器、模块系统 - 学会搭建大型"积木建筑"
- 第四天:高级类型操作、工具类型、类型编程 - 学会"积木魔法"
- 第五天:现代JavaScript特性与TypeScript集成 - 学会将"积木"与现代工具完美融合
学习建议:
- 多写代码,在实践中巩固理论知识
- 启用strict模式,让TypeScript更严格地帮你检查错误
- 看看优秀项目的TypeScript代码,学习别人怎么写
- 试着把现有的JavaScript项目改成TypeScript
通过这五天的学习,你将从TypeScript新手变成能够熟练使用TypeScript的开发者!