TypeScript基础完整梳理:从环境配置到泛型迁移
发布时间:2026/9/28 15:04:33来源:尧图网络
很多前端同学第一次接触 TypeScript 都是这样项目里要用教程零散语法背了又忘写着写着就开始用 any。我刚开始也有过这种状态直到后来真正理解了类型系统才明白 TS 不是给代码添麻烦而是给 JavaScript 补上“形状描述”。这篇内容就是 TypeScript 基础部分的完整梳理我会从痛点讲起把环境配置、基础类型、接口、泛型、类、迁移和在线工具全部过一遍顺便把最近很多人遇到的baseurl、moduleResolutionnode10弃用警告也讲清楚。不管你是刚入行的前端新手还是被项目逼着补 TS 的 JavaScripter照着这条路走基本不会偏。1. 别急着敲代码先搞懂 TypeScript 到底在解决什么1.1 动态类型不是原罪工程化才是JavaScript 本身的动态类型在小型页面时代很好用变量想存什么就存什么函数参数传对传错全靠自觉。但项目一旦超过几个模块、几个人协作这种“自由”就会快速变成负担。最常见的场景是别人写了一个函数你拿到手里不知道参数该传什么只能去翻源码或者一遍遍 console.log 试错。比如这个很经典的例子function merge(a, b) { return a b; } merge(用户, 1); // 用户1 merge({ id: 1 }, { name: 张三 }); // [object Object][object Object]JavaScript 不会在运行前给你任何提示merge收到两个对象时依然能跑只是结果是完全没意义的字符串。这种问题在数据流复杂的业务系统里会被放大接口返回的数据结构一旦和前端预期不一致排查起来往往要花掉半天时间。TS 做的事情就是在代码还没运行之前把这些“形状不匹配”的问题暴露出来。1.2 类型系统带来的三个直接收益第一个收益是编辑器提示。安装 TS 后VSCode 对变量的自动补全、跳转定义、重命名符号都会好用很多因为编辑器知道你正在操作的到底是什么类型。第二个收益是重构安全。项目大了以后重命名一个字段或函数靠“全局搜索 手工替换”很容易漏掉某处引用TS 会在编译时报错告诉你哪里还没改完。第三个收益是接口契约。团队协作时类型声明本身就是文档新同学看一个函数签名就能知道入参出参不用去读实现细节。很多人一开始学 TS 喜欢把重点放在背语法上但语法只是表面。真正值钱的是建立起“用类型描述数据边界”的思维。带着这种思维去看后面的基础语法你才会觉得每个知识点都有它的位置。TS 是 JavaScript 的超集编译后依然是 JS不会改变运行时行为这也是它能被大规模落地的前提。2. 搭一套“不淘汰”的开发环境从 Node 到 tsconfig2.1 基础工具链安装学习 TS 最基础的工具链就三样Node.js、npm 和 TypeScript 编译器。Node.js 可以去官网下载 LTS 版本npm 会随 Node 一起装上。TS 本身是一个 npm 包全局安装方便命令行随处使用npm install -g typescript tsc --version不过我更推荐在具体项目里局部安装这样版本可控不会因为全局版本影响多个项目。初始化一个工程可以这样来mkdir ts-basic cd ts-basic npm init -y npm install typescript --save-dev npx tsc --init执行完npx tsc --init会生成一个 tsconfig.json这是 TS 项目的核心配置文件。很多新手把它当成“生成的默认配置能用就行”其实这里藏着一堆后来让你头疼的坑比如版本升级后的弃用警告。2.2 tsconfig.json 里的核心选项建议直接理解tsconfig.json 的选项很多但基础阶段真正需要理解的没几个。我整理了一张常用表选项作用推荐值target编译输出的 ECMAScript 版本ES2022或ES2020module模块化方案ESNext现代打包器或NodeNextmoduleResolution模块解析策略bundler/nodenext/node16strict开启所有严格类型检查trueoutDir编译产物输出目录./distrootDir源码根目录./srcskipLibCheck跳过声明文件类型检查true能省编译时间strict必须重点说。很多老教程为了降低学习门槛会让你暂时不开启 strict但这样你写的代码很多类型漏洞并不会被暴露出来。我建议从第一天就把strict: true打开养成的习惯是变量可能为 null 就处理 null数组可能为空就考虑空数组。严格模式逼着你把边界想清楚这个习惯在写业务代码时非常值钱。2.3 baseurl 与 moduleResolution 弃用警告最近最容易踩的坑最近升级 TS 版本的同学可能会在编译时看到这样的警告选项“baseurl”已弃用并将停止在 typescript 7.0 中运行。指定 compilerOption 以替代。 选项“moduleresolutionnode10”已弃用并将停止在 typescript 7.0 中运行。指定 compilerOption 以替代。这不是报错但意味着你的配置在未来的 TS 7.0 里会失效。先说baseurl。老项目常这样配置路径别名{ compilerOptions: { baseUrl: ., paths: { /*: [src/*] } } }baseUrl: .让所有相对路径解析都基于项目根目录配合paths实现/xxx这样的别名。但官方后来发现baseUrl的存在会让路径解析变得隐式、复杂尤其是大型 monorepo 里很容易出现“按 tsconfig 位置算还是按 baseUrl 算”的歧义。所以新版本建议直接去掉baseUrlpaths里的相对路径会自动基于 tsconfig.json 所在目录。改造后的写法是{ compilerOptions: { paths: { /*: [./src/*] } } }再说moduleResolutionnode10。node10是旧的 Node.js 模块解析模式也就是 CommonJS 时代从node_modules找包的算法。现在的前端项目不是 Node 原生 ESM就是 Vite、Webpack 打包器老的node10解析跟不上实际运行环境。官方推荐这么选纯 Node 原生 ESM 工程module: NodeNext、moduleResolution: NodeNext使用 Vite、Webpack 等打包器moduleResolution: bundler如果你用的是 tsc 默认生成的配置大概率还是moduleResolution: node10看到弃用警告就说明该改成bundler或nodenext了。处理掉这两个警告你的项目在 TS 7.0 到来时也能平滑升级这也是现在面试时喜欢追问的细节。3. 基础类型全拆解把值装上“形状”3.1 原始类型与显式注解TS 的原始类型和 JS 一一对应number、string、boolean、null、undefined、symbol、bigint。给变量加类型非常简单let count: number 42; let username: string 张三; let isAdmin: boolean false; let nothing: null null; let notDefined: undefined undefined;这里有个初学者常问的问题null和undefined到底有什么差别在严格模式下null表示“有意的空值”undefined表示“未赋值”。更常见的是使用联合类型比如一个函数可能返回字符串也可能什么都没拿到就写成string | undefined。除了显式注解TS 还支持类型推断。如果你直接写let count 42TS 会自动推断count: number。所以并不是每个地方都要写类型变量声明时如果初始值很清楚可以不写注解。但函数参数和返回值建议都写因为函数是模块之间的边界边界清晰整体才不容易乱。3.2 数组、元组与对象类型数组类型有两种写法一个用方括号一个用泛型const list1: number[] [1, 2, 3]; const list2: Arraystring [a, b, c];如果你要表示一个固定长度、每个位置类型固定的数组就要用元组。元组在处理坐标点、CSV 行、键值对等场景时非常实用const point: [number, number] [10, 20]; const userInfo: [string, number] [张三, 30];对象类型是日常使用频率最高的类型之一。你可以直接定义内联对象类型const user: { name: string; age: number; email?: string; } { name: 张三, age: 30, };email?: string表示email是可选的可能有也可能没有。这比 JS 里到处判断if (user.email)要严谨得多TS 知道它是可选之后你在使用user.email时如果没做空值判断编译器就会提醒你。这是很多新手上手后觉得“TS 很啰嗦”的原因但也是它真正保护你的地方。3.3 字面量类型、联合类型与类型收窄字面量类型的意思是把某个具体的值当作类型来用。比如一个状态字段只希望它接收success、error、loading三个值type Status success | error | loading; let status: Status success;这里的|是联合类型表示“或者”。联合类型可以组合值也可以组合类型type ID string | number; let userId: ID abc123; userId 123;当你拿到一个联合类型时TS 需要知道程序运行的某个分支里到底具体是什么类型这个过程叫类型收窄。最简单的收窄方式是typeoffunction formatId(id: string | number) { if (typeof id string) { return id.toUpperCase(); } return id.toFixed(0); }写条件判断不是为了写而写它是让 TS 能在不同分支里推断出更精确的类型。理解了类型收窄你就能摆脱“到处 as any”的坏习惯。3.4 类型推断、类型断言与 as const类型推断是 TS 最友好的特性它会在你不写注解时自动推导类型。比如const names [张三, 李四]会被推断成string[]。但有些场景推断的结果可能不是你想要的尤其当变量初始值比较泛或会被重新赋值时最好手动注解。类型断言是“告诉 TS 相信我写的类型”语法是asconst inputEl document.getElementById(input) as HTMLInputElement;这条代码很常见因为getElementById返回的是HTMLElement | null你需要断言成具体的HTMLInputElement才能访问.value。但断言不能滥用如果一个对象和你断言出来的类型完全不相干TS 也会报错这说明你写错了而不是编译器太严格。as const是一个很有用的断言它会把字面量变成只读的const mode dark as const;这样mode的类型就是dark而不是string后续不能再被改成别的值。这在配置常量、事件名枚举里很常用比单纯写字符串更安全。3.5 枚举能用但要想清楚枚举enum也是 TS 原生的语法分数字枚举和字符串枚举enum Color { Red, Green, Blue, }数字枚举默认从 0 开始递增反向映射是它比较特别的行为。字符串枚举没有反向映射可读性更好enum StatusCode { Success SUCCESS, Error ERROR, }现在很多 TS 风格指南推荐用as const对象代替枚举原因是枚举的语义和编译产物略显复杂as const对象更直观也更容易和联合类型配合。我的建议是如果你的团队没有明确规范基础阶段先学会用枚举面试时能说出它和as const的区别就够了实际项目里尽量用as const对象减少心智负担。4. interface 和 type描述复杂数据的两种姿势4.1 interface约定数据形状接口是 TS 描述对象结构的核心语法和对象类型很像但用途更明确。一个接口通常代表一份“契约”interface User { id: number; name: string; email?: string; readonly createdAt: string; }readonly表示属性只能在初始化时赋值之后不能再修改。这在接口返回的数据模型里很常用你拿到一条用户记录后不应该随手把createdAt改掉。接口之间可以继承interface Admin extends User { permissions: string[]; }这样Admin就自动拥有了User的所有字段又额外增加了permissions。接口还有合并声明的能力同一个接口在多个地方声明会自动合并这是某些老插件系统需要的特性但业务代码里不建议依赖它。4.2 type alias更自由地组合类型类型别名type的核心作用是给类型起一个新名字但它能做的远不止对象描述type ID string | number; type Point { x: number; y: number }; type UserOrAdmin User | Admin;从 TS 近些年的版本趋势看type能做的事情越来越多而interface的合并声明能力在业务代码中反而很少用到。有人总结过经验对外定义公共数据结构时用interface内部做类型组合、联合类型时用type。我比较认可这个习惯但也建议你把两者都练熟因为面试官经常问区别甚至会在白板上考你什么时候会选 type、什么时候选 interface。4.3 函数类型签名参数和返回值都要管函数在 TS 里是第一等公民所以它也可以有类型。最简单的写法是在函数声明里标注参数和返回值function add(a: number, b: number): number { return a b; }当函数作为变量传递或者作为回调参数时可以用类型别名描述函数形状type AddFn (a: number, b: number) number; const add: AddFn (a, b) a b;这里要注意TS 的和 ES6 箭头函数不是一回事它只是类型层面的函数签名表示。还有一个常用概念是回调函数比如数组的map、filter正确标注回调参数类型后TS 能帮你推断出返回值的类型写起来非常舒服const numbers: number[] [1, 2, 3]; const dubbled numbers.map((n) n * 2);这里的n你甚至不用写类型TS 会根据map的签名自动推断为number。能体会到这种“自动联动”基本就找到 TS 开发的感觉了。4.4 泛型入门让类型也支持参数泛型是 TypeScript 基础阶段最重要也最容易懵的一个知识点。简单说泛型允许你在定义函数、接口或类的时候先不写死具体类型在使用时才确定类型。最经典的例子function identityT(value: T): T { return value; } const str identitystring(hello); const num identitynumber(42);这里的T就像类型层面的参数它保证了输入和输出的类型相同。如果把identity写死成string那传给数字就会丢失类型。泛型最常见的应用之一是约束函数参数结构function getPropertyT, K extends keyof T(obj: T, key: K) { return obj[key]; }这里的K extends keyof T是泛型约束它让参数key只能是对象obj自身已有的键。如果你传入一个不存在的键TS 会直接编译报错。泛型一开始不要求全懂但要先能看懂社区代码里常见的T知道它代表“类型参数”后面进阶再慢慢深入。5. 类与面向对象让 TS 的“类”真正有约束力5.1 public/private/protected 访问修饰符ES6 引入了class但 JavaScript 的类缺少访问控制所有属性默认都能从外部改。TS 为此加了三种访问修饰符class Base { public name: string; private secret: string; protected shared: string; constructor(name: string, secret: string, shared: string) { this.name name; this.secret secret; this.shared shared; } } const base new Base(a, b, c); base.name; // ok // base.secret; // 报错属性“secret”为私有属性public是所有地方都能访问默认就是它private只能在类内部访问protected只能在类内部和继承它的子类中访问。这套机制让类的“边界”清晰起来外部代码不再能随便碰内部状态。很多新手觉得private没用但在多人协作时它确实能阻挡很多“非预期的修改”。5.2 参数属性与 readonlyTS 给构造函数的参数提供了一种语法糖可以在参数前直接加访问修饰符自动变成类属性class User { constructor( public id: number, public name: string, readonly createdAt: Date new Date() ) {} } const user new User(1, 张三); user.id; // 1 user.createdAt new Date(); // 报错readonly 属性不可赋值这种写法省去了手写属性声明和构造体内赋值的冗余代码在 Angular、NestJS 这类依赖注入框架里非常常见。readonly和访问修饰符可以组合使用表示属性只读。5.3 抽象类与接口实现抽象类是不能被实例化的类它定义子类必须实现的方法也可以自带已经实现好的公共逻辑abstract class Shape { abstract getArea(): number; describe() { return 面积是 ${this.getArea()}; } } class Circle extends Shape { constructor(private radius: number) { super(); } getArea(): number { return Math.PI * this.radius * this.radius; } }这里abstract getArea()只声明了方法签名具体实现必须由子类完成。抽象类和接口的区别在于接口只是“形状描述”不包含任何实现抽象类可以包含公共逻辑。接口是面向对象的另一核心类可以实现多个接口interface Flyable { fly(): void; } interface Swimmable { swim(): void; } class Duck implements Flyable, Swimmable { fly() {} swim() {} }TS 的类体系能让你感受到它不只是给 JS 加类型还在把“更严谨地设计对象关系”这件事补回来。前端项目很少用深度的类继承但理解这些基础对你阅读源码、使用类的设计模式都很有帮助。6. 从 JS 项目迁移到 TS顺序、误区与常见报错6.1 三步迁移法很多团队不是从零开始写 TS而是要把存量 JS 项目分阶段迁移过来。我这边的经验是三步走第一步把.js文件改名为.ts先把编译跑通暂时不要求所有代码都有正确类型第二步逐个文件补类型声明重点优先补公共函数、核心状态层和接口数据第三步再开启strict: true并处理所有严格模式报错。这个顺序的合理性在于先让工具链和模块解析跑通避免“改完类型还跑不起来”的双重挫折然后通过补类型建立项目的数据模型认知最后用严格模式兜底把null、可选链这些边界问题彻底过一遍。迁移期间可以临时使用any但不要让它成为逃出口每用一个any都值得在旁边写注释说明原因。6.2 第三方库的类型缺口JS 生态里大量包没有自带类型声明这时候你需要安装对应的types包。比如 Node.js 相关代码需要types/nodeReact 需要types/reactnpm install -D types/node如果某个库连types都没有你可以在项目里为它声明模块类型declare module old-lib { export function doSomething(input: string): void; }这种写法是“告诉 TS 这个库对外长什么样”属于类型声明的入门操作。迁移阶段遇到类型缺失时先别慌先查types再查官方文档里的类型定义文件最后再考虑自己声明。第三方库的类型问题处理得好不好直接影响迁移效率。6.3 常见编译错误基础阶段最常遇到的编译错误我整理成了表格报错场景原因解决方式TS2531: Object is possibly null你可能访问了可为 null 对象的属性先判空或用可选链?.TS2322: Type X is not assignable to type Y类型不兼容检查数据结构是否对齐必要时用联合类型或重构TS2304: Cannot find name xxx变量未声明 / 类型声明缺失检查 import检查全局类型声明TS7016: Could not find a declaration file for module第三方库没类型声明安装types/*或declare module遇到报错不要急着as any先读错误信息里的类型名。TS 的报错其实已经精确告诉了你“期望什么、实际是什么”这是它送你的一份调试信息看懂之后就离修好不远了。尤其是 nullable 相关报错本质上是提醒你代码里存在边界情况没处理用!非空断言可以压下去但不如老老实实写一次判断更稳妥。7. 在线验证与日常训练Playground、tsx 和编码习惯7.1 TypeScript 演练场零成本练手TypeScript 官方提供了一个在线编辑器叫 TypeScript Playground中文社区一般叫“TS 演练场”。它的好处是零安装、立刻运行还能看到编译后的 JS 和类型推导过程。练基础语法时遇到不确定的写法我很多时候不翻文档直接打开演练场写几行代码看结果。Playground 左上角可以调整TS版本右下角能显示Types面板查看某个表达式的类型。学类型收窄、泛型推断时这个工具比任何教程都有说服力你亲眼看到typeof分支之后类型变了才算真正记住。面试前我也会拿它快速过一遍keyof、typeof、in这些操作符的用法。7.2 快速运行 TS 脚本如果你在命令行里想直接运行.ts文件不建议每次都编译再跑。两个常用工具是ts-node和tsx。比较推荐tsx它对 ESM 和现代模块配置的支持更好启动也快npm install -D tsx npx tsx src/index.ts这样改完代码一运行就能看到结果适合写一些小算法、验证类型逻辑。写脚本时可以故意写错类型看看编译器和运行时给不给反馈这也是一个很好的训练方式。等真正进入项目开发再让打包工具Vite、Webpack、tsc处理正式编译。7.3 几个值得坚持的编码习惯最后聊几个我从实际项目里总结的习惯都是基础阶段就能开始培养的。第一个是“函数参数和返回值必写类型”哪怕内部实现可以靠推断外部边界也尽量清晰。第二个是“少用 any多用 unknown”unknown强迫你先收窄再使用比any安全得多。第三个是“能用类型收窄就别用断言”断言相当于告诉 TS 你别管了用得越多类型保护越鸡肋。还有一个细节命名。类型名和接口名用 PascaCase比如UserInfo、GetUserRequest变量和函数用 camelCase。命名规范看似小事但团队项目里类型名称混乱比没有类型还难维护。编码规范这件事我建议直接阅读一些主流团队的 TS 风格指南比如 ESLint 推荐的typescript-eslint/consistent-type-imports等规则从工具层面强制自己养成习惯。我自己的体会是TypeScript 基础阶段不用追求把每个高级类型都背下来核心是把“数据形状”这件事想清楚。环境配置、类型语法、接口泛型、类、迁移这一条线走通以后后面学工具类型、装饰器、条件类型都会快很多。你现在用 Playground 把今天这些例子敲一遍比盯着文章看十遍有用得多。
网站建设高端定制企业官网