TypeScript 完全指南:从入门到精通

📅 2026/7/16 3:31:10
TypeScript 完全指南:从入门到精通
告别“AnyScript”用 TS 写出更健壮的代码前言为什么我们需要 TS如果你是一名前端开发者大概率经历过这样的场景// 一段看似正常的 JS 代码 function greet(user) { return Hello, user.name; } greet(null); // 程序炸了Cannot read property name of null这种在运行时才暴露错误的问题在 JavaScript 中太常见了。JS 最初的定位是简单的浏览器脚本语言代码量很小。但如今JS 已经可以全栈开发代码量呈指数级增长当年“出生简陋”的 JS在面对大型应用时就显得力不从心了。TypeScript 的出现就是为了解决 JavaScript 的“痛点”不清不楚的数据类型你永远不知道一个变量到底是个啥。逻辑上的漏洞比如上面那个例子漏掉了空值判断。访问不存在的属性对象里根本没这个 key你却偏要访问。低级的拼写错误把onClick写成onClcik找半天 bug。TS 的核心价值在于它提供了一套静态类型检查系统在代码运行之前编译阶段就帮你发现这些潜在的错误。说白了TS 就是 JS 的“盔甲”穿上它你写的代码会更健壮重构起来也更有底气。第一章快速上手 TS 环境在开始写 TS 之前我们需要先搭建好环境。1.1 安装 TypeScriptTypeScript 最终会被编译成 JavaScript所以我们需要一个编译器。# 全局安装 TypeScript npm i typescript -g # 检查是否安装成功 tsc -v1.2 编译与配置安装完成后我们就可以使用tsc命令来编译.ts文件了。# 编译一个 ts 文件 tsc index.ts # 初始化 tsconfig.json 配置文件控制 tsc 的行为 tsc --init # 监听文件变化实时编译 tsc --watch小技巧默认情况下即使 TS 代码里有类型错误tsc也会强行编译成 JS 文件。如果你希望在有类型错误时阻止编译可以在tsconfig.json中开启noEmitOnError选项{ compilerOptions: { noEmitOnError: true } }第二章TS 的核心——类型系统类型系统是 TS 的精髓所在掌握它你就掌握了 TS 的 80%。2.1 基础类型与类型声明TS 支持与 JS 几乎相同的数据类型并额外增加了一些新类型。// 基本类型声明 let a: string; let b: number; let c: boolean; // 字面量类型值本身即是类型不能更改 let hello: hello; hello world; // ❌ 报错不能将类型world分配给类型hello // 类型推论TS 能根据赋值自动推导类型 let d 99; // TS 自动推导 d 为 number 类型 // d hello; // ❌ 报错类型不匹配2.2anyvsunknownvsnever这三个类型是 TS 新增的也是初学者最容易混淆的地方。any类型系统的“黑洞”它可以赋值给任意类型也可以被任意类型赋值。不推荐使用因为它会完全失去类型检查让 TS 变成了“AnyScript”。unknown类型安全的“any”它可以存储任意类型的值但不能直接赋值给其他类型需要先进行类型断言或类型守卫。let unknownVar: unknown; let strVar: string; // strVar unknownVar; // ❌ 报错unknown 类型不能赋值给 string strVar unknownVar as string; // ✅ 使用类型断言 strVar stringunknownVar; // ✅ 另一种断言写法never表示“永远不存在”的值通常用于函数抛出异常或死循环。function throwError(msg: string): never { throw new Error(msg); } // 几乎用不到因为它更多是 TS 内部使用比如做完整性检查。金句any是魔鬼unknown是保镖。能用unknown就别用any。2.3void与函数返回值void表示函数没有返回值。但有一个小细节需要注意type VoidFunc () void; let f1: VoidFunc function() { return 666; // ✅ 允许但返回值会被忽略 }; let f2: VoidFunc () 123; // ✅ 也允许箭头函数简写隐式返回 // 虽然可以返回其他值但不建议依赖这个返回值 const result f1(); // result 类型是 void而不是 number为什么 TS 不强制void函数必须返回undefined这是为了兼容 JS 的习惯比如数组的forEach中你经常写() arr.push(1)这其实隐式返回了一个数字但没人会去用这个返回值。2.4 对象、数组与函数类型声明对象类型// 固定属性 let person: { name: string, age?: number }; // ? 表示可选属性 // 索引签名属性名不确定 let dict: { [key: string]: any }; dict { name: lgl, gender: male }; // 注意object 和 Object 的区别 let a: object; // 只能存非原始类型对象、数组等 let b: Object; // 可以调用 Object 方法的类型不能存 null、undefined // 实际开发中这两个很少直接用更推荐上面那种字面量或接口方式数组类型let arr1: number[] [1, 2, 3]; let arr2: Arraynumber [1, 2, 3]; // 泛型写法函数类型// 声明一个函数类型 let count: (x: number, y: number) number; count (x, y) x y;2.5 元组Tuple与枚举Enum元组固定长度、固定类型的数组。let tuple: [string, number] [hello, 123]; // tuple [123, hello]; // ❌ 报错顺序不匹配枚举一组命名常量增强代码可读性。enum Color { Red, // 默认 0 Green, // 默认 1 Blue // 默认 2 } console.log(Color.Red); // 0 // 如果不想生成多余的 JS 代码可以使用常量枚举编译后直接内联替换 const enum Gender { Male 男, Female 女 } let myGender Gender.Male; // 编译后直接变成 男2.6type与接口Interface的灵活用法联合类型与交叉类型// 联合类型或 type Status number | string; type Gender 男 | 女; // 交叉类型且 type Person { name: string } { age: number }; let p: Person { name: lgl, age: 18 };类型别名 vs 接口type可以声明联合类型、交叉类型等更灵活。interface更适合声明对象的形状且可以重复声明合并。interface User { name: string; } interface User { age: number; } // User 最终类型为 { name: string; age: number; }第三章面向对象——类Class与抽象类TS 对 ES6 的类进行了增强提供了完整的面向对象支持。3.1 类的构造器简写传统的写法比较冗余class Person { name: string; age: number; constructor(name: string, age: number) { this.name name; this.age age; } }一键简写在构造器参数前加上访问修饰符public、private、protectedTS 会自动帮我们定义并赋值。class Person { constructor(public name: string, public age: number) {} } const p new Person(lgl, 18); console.log(p.name); // 直接访问3.2 抽象类Abstract Class抽象类是用来被继承的它不能实例化可以包含具体实现也可以包含抽象方法只有定义没有实现。abstract class Animal { abstract makeSound(): void; // 抽象方法子类必须实现 move(): void { console.log(roaming the earth...); } } class Dog extends Animal { makeSound(): void { console.log(汪汪汪); } } // const animal new Animal(); // ❌ 报错无法创建抽象类的实例抽象类 vs 接口特性抽象类接口是否包含实现代码✅ 可以❌ 不可以继承/实现数量只能继承一个可以实现多个适用场景提供通用基类复用代码定义契约描述结构第四章进阶——泛型Generics泛型允许你在定义函数、类、接口时不预先指定具体类型而是在使用时再确定。这是一个极其强大的工具。// 一个“万能”的 identity 函数 function identityT(arg: T): T { return arg; } // 使用时显式指定类型 let output1 identitystring(myString); // 或利用类型推论自动推导 let output2 identity(123); // output2 是 number 类型泛型也可以用在类和接口中// 泛型类 class StackT { private items: T[] []; push(item: T) { this.items.push(item); } pop(): T | undefined { return this.items.pop(); } } const numStack new Stacknumber(); numStack.push(1); // numStack.push(a); // ❌ 报错 // 泛型接口 interface KeyValueK, V { key: K; value: V; } const kv: KeyValuestring, number { key: age, value: 18 };金句泛型让代码从“一劳永逸”变成“一劳多逸”一次定义多种类型复用。第五章装饰器Decorator——TS 的黑魔法装饰器是 TS 在 2.0 版本引入的一个重要特性它是一个特殊的函数可以“装饰”类、方法、属性等。注意装饰器是一项实验性特性在tsconfig.json中需要开启experimentalDecorators选项。记住一个口诀类装饰器管构造方法装饰器管行为访问器管 get/set属性管定义参数管顺序。5.1 类装饰器Class Decorator应用场景修改类的构造函数、添加静态属性、替换类本身。入参构造函数的构造函数即target: Function。// 场景给类打上版本号标签 function addVersion(version: string) { return function T extends { new(...args: any[]): {} }(constructor: T) { return class extends constructor { version version; // 还可以添加静态属性 static apiUrl /api/v2; }; }; } addVersion(1.0.0) class UserApi { constructor(public name: string) {} } const user new UserApi(lgl); console.log((user as any).version); // 1.0.0 console.log((UserApi as any).apiUrl); // /api/v25.2 方法装饰器Method Decorator应用场景日志记录、性能监控timer、权限校验、修改返回值。入参(target, propertyKey, descriptor)。// 场景权限校验装饰器 function checkRole(role: string) { return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { const originalMethod descriptor.value; descriptor.value function (...args: any[]) { // 模拟从上下文中获取当前用户 const currentUser { name: lgl, role: admin }; if (currentUser.role role) { return originalMethod.apply(this, args); } else { throw new Error(权限不足需要 ${role} 角色); } }; return descriptor; }; } class OrderService { checkRole(admin) deleteOrder(id: number) { console.log(订单 ${id} 已删除); } } const service new OrderService(); service.deleteOrder(1001); // ✅ 正常执行5.3 访问器装饰器Accessor Decorator应用场景拦截 Getter/Setter 的读写操作比如数据劫持、防抖。入参与方法装饰器完全相同(target, propertyKey, descriptor)。// 场景阻止属性被频繁修改防抖 setter function debounceSet(wait: number) { return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { const originalSet descriptor.set!; let timer: NodeJS.Timeout | null null; descriptor.set function (value: any) { if (timer) clearTimeout(timer); timer setTimeout(() { originalSet.call(this, value); timer null; }, wait); }; return descriptor; }; } class SearchBox { private _keyword: string ; get keyword() { return this._keyword; } debounceSet(300) set keyword(val: string) { this._keyword val; console.log(搜索关键词更新为: ${val}); // 只会延迟 300ms 后输出最后一次 } } const box new SearchBox(); box.keyword a; box.keyword ab; box.keyword abc; // 只有这次会在 300ms 后触发打印5.4 属性装饰器Property Decorator应用场景为属性定义元数据、映射数据库字段、依赖注入DI。入参(target, propertyKey)——没有第三个参数且无法直接修改属性的值通常配合reflect-metadata库使用。// 场景标记字段对应的数据库列名 function Column(name: string) { return function (target: any, propertyKey: string) { // 在原型上存储元数据 if (!target.constructor.__columns) { target.constructor.__columns {}; } target.constructor.__columns[propertyKey] name; }; } class UserEntity { Column(user_name) name: string; Column(user_age) age: number; } // 模拟 ORM 读取映射关系 console.log((UserEntity as any).__columns); // { name: user_name, age: user_age }5.5 参数装饰器Parameter Decorator应用场景在 NestJS / Java Spring 中极为常见用于获取路由参数Param(id)、注入服务等。入参(target, methodKey, parameterIndex)。// 场景标记参数为需要自动注入 function InjectUser(target: any, methodKey: string, parameterIndex: number) { if (!target.__injectParams) { target.__injectParams {}; } if (!target.__injectParams[methodKey]) { target.__injectParams[methodKey] []; } target.__injectParams[methodKey].push(parameterIndex); } class OrderController { getOrders(InjectUser userId: number) { console.log(查询用户 ${userId} 的订单); } } // 模拟框架在调用时发现 __injectParams自动从 session 中取出 userId 填进去5.6 装饰器执行顺序小贴士当多个装饰器作用于同一个目标时执行顺序为属性/方法/参数装饰器先执行从下到上 →类装饰器最后执行// 执行顺序参数装饰器 - 方法装饰器 - 类装饰器 ClassDecorator class Example { MethodDecorator method(ParamDecorator param: any) {} }第六章TypeScript 高级类型编程解锁“类型体操”只会写interface和type只是入门。真正的中级进阶是学会操作类型把类型当成“值”来处理。6.1 工具类型Utility Types——TS 官方自带的“万能工具箱”TypeScript 内置了几个超级好用的工具类型直接帮我们省去大量重复定义。工具类型作用实战场景PartialT将 T 中所有属性变为可选更新数据时只传部分字段RequiredT将 T 中所有属性变为必选强制补全配置项PickT, K从 T 中挑选几个属性组成新类型只取接口返回的某几个字段OmitT, K从 T 中剔除几个属性组成新类型排除敏感字段如密码RecordK, T构造一个对象类型键为 K值为 T做字典映射ReadonlyT将 T 中所有属性变为只读防止对象被意外修改interface User { id: number; name: string; password: string; email: string; } // 场景1更新时只用传 id 和想改的字段 type UpdateUserParams PartialOmitUser, id { id: number }; // 场景2前端展示列表去掉敏感 password type PublicUser OmitUser, password; // 场景3做个国家编码映射 type CountryMap Recordstring, string; const map: CountryMap { CN: 中国, US: 美国 };金句Pick和Omit是你处理庞大后端接口返回数据时的最佳战友能极大增强前端的类型安全性。6.2 映射类型Mapped Types——定制自己的工具类型明白了Partial的原理你就能创造自己的工具类型。// 手写一个 Partial原理揭秘 type MyPartialT { [P in keyof T]?: T[P]; }; // 手写一个 Readonly只读 type MyReadonlyT { readonly [P in keyof T]: T[P]; }; // 进阶给所有属性加上前缀比如转成 getter 风格 type GettersT { [P in keyof T as get${Capitalizestring P}]: () T[P]; }; // 如果 T 有 name: string会生成 getName: () string6.3 条件类型Conditional Types——类型的“if/else”条件类型让类型具备逻辑判断能力语法是T extends U ? X : Y。// 场景根据泛型自动推断返回的数组类型 type ElementTypeT T extends any[] ? T[number] : T; type A ElementTypestring[]; // A 为 string type B ElementTypenumber; // B 为 number // 高级结合过滤掉函数类型 type NonFunctionPropertyNamesT { [K in keyof T]: T[K] extends Function ? never : K; }[keyof T]; interface Mixed { name: string; age: number; eat: () void; } type OnlyProps NonFunctionPropertyNamesMixed; // name | age6.4infer关键字——类型推断的“占位符”infer是条件类型里的“占位符”用于提取类型的某一部分。// 提取函数返回值类型手写 ReturnType type MyReturnTypeT T extends (...args: any[]) infer R ? R : never; function getUser() { return { id: 1, name: lgl }; } type UserType MyReturnTypetypeof getUser; // { id: number; name: string; } // 提取数组元素类型 type MyArrayElementT T extends (infer U)[] ? U : never; type Str MyArrayElementstring[]; // string6.5 类型守卫Type Guards——让 TS 在代码块内“变聪明”有时候 TS 无法自动推断联合类型的具体分支需要我们用is关键字来收窄类型。// 自定义类型守卫 function isString(value: any): value is string { return typeof value string; } function process(input: string | number) { if (isString(input)) { // 在这个块里TS 知道 input 一定是 string console.log(input.toUpperCase()); } else { console.log(input.toFixed(2)); } } // 另一种方式typeof 类型守卫 function process2(input: string | number) { if (typeof input string) { console.log(input.length); // TS 自动收窄为 string } }6.6 内置高级类型补充工具类型作用ParametersT获取函数 T 的参数类型元组形式ReturnTypeT获取函数 T 的返回值类型InstanceTypeT获取构造函数 T 的实例类型NonNullableT从 T 中剔除null和undefinedfunction greet(name: string, age: number): string { return ${name} is ${age} years old; } type GreetParams Parameterstypeof greet; // [string, number] type GreetReturn ReturnTypetypeof greet; // string第七章实战最佳实践总结7.1 开发建议始终开启strict模式在tsconfig.json中开启严格模式它会帮你检查更多潜在问题。避免使用any把它当作最后的手段优先考虑unknown或更具体的类型。善用类型推导能自动推导出来的类型就不用显式声明保持代码简洁。使用interface定义对象形状使用type定义联合类型。泛型是你的好帮手写可复用的组件、函数时优先考虑泛型。装饰器谨慎使用虽然强大但过度使用会降低代码可读性适合框架层使用。7.2 tsconfig.json 推荐配置快速上手版{ compilerOptions: { target: ES2020, module: commonjs, strict: true, esModuleInterop: true, skipLibCheck: true, forceConsistentCasingInFileNames: true, experimentalDecorators: true, emitDecoratorMetadata: true, noEmitOnError: true, outDir: ./dist, rootDir: ./src }, include: [src/**/*], exclude: [node_modules, dist] }写在最后高阶心法类型编程不是炫技在团队协作中可读性永远大于“骚操作”。如果写一个复杂的泛型能让调用方爽到那就写如果只是自己爽别人看吐血那请加上详细注释。理解keyof和inkeyof取键的联合类型in遍历联合类型。这是映射类型的基石。TS 版本迭代很快关注 TypeScript 官方 Release Notes每个版本都会带来新工具类型和语法糖保持学习。TypeScript 就像是 JavaScript 的“静态显微镜”语法是皮类型思维是骨。当你习惯了用类型去“约束”和“推导”时你就会发现代码写完即完工Bug 无处遁形。