
TypeScript 基础类型——这个东西听起来好像就是“给变量加个类型”而已但等你真正上手写一段时间会发现它其实是整个 TypeScript 世界里最值得花时间啃透的部分。你可以不背住每个工具类型的实现但基础类型如果理解得不扎实后面看函数重载、泛型、类型体操这些内容的时候基本会一脸懵。这篇内容不是官网手册的翻译而是我结合自己在项目里实际踩过坑之后总结的学习笔记适合刚接触 TypeScript 的前端开发者也适合那些已经写了几个月 JS、准备把代码迁移到 TS 的朋友。我先把话放在前面TypeScript 的基础类型并不难难的是你搞不懂“为什么要这样设计”。所以这篇文章我会在讲每个类型的同时解释它背后的逻辑再给你看实际代码。最后一部分是我整理的一些配置和报错排查经验包括最近挺多人碰到的 tsconfig 里 baseUrl 弃用提示我都会聊到。1. 基础类型TypeScript 的“原话”1.1 为什么 TypeScript 非要搞一套基础类型很多从 JavaScript 转过来的人一开始都会有个疑问JS 本身已经有 number、string、boolean 这些类型了为什么 TypeScript 还要再声明一遍这个问题其实问到了点子上。JavaScript 里的类型是运行时才确定的同一个变量今天可能是字符串明天可能就是数字。你写let a 1;然后a 2;JS 不会拦你只会让你在后面的逻辑里自己收拾烂摊子。TypeScript 做的事情是把这层检查提前到代码编译阶段让你在写代码的时候就明确这个变量能装什么东西、不能装什么东西。所以 TypeScript 的基础类型本质上就是“给 JavaScript 的动态值加上静态标签”。加了这个标签之后编辑器能给你更靠谱的补全提示重构的时候能帮你找到所有引用点更重要的是大量低级错误能在保存代码的一瞬间暴露出来而不是等线上用户操作到某条分支才炸掉。我自己的体会是能不能用好 TypeScript关键是能不能理解“类型是一种约束”这个观念。它不是让你多写几行字而是让你在描述数据的时候更严谨。1.2 每个基础类型的真实行为TypeScript 的基础类型和 JavaScript 原始类型一一对应常见的就这几个boolean、number、string、null、undefined、symbol、bigint。下面用一个表格把它们的典型用法列出来直观点类型写法示例说明booleanlet isDone: boolean false;只有 true / falsenumberlet count: number 42;整数、浮点数、NaN、Infinity 都算stringlet name: string typescript;单引号、双引号、模板字符串都行nulllet n: null null;只能赋值为 nullundefinedlet u: undefined undefined;只能赋值为 undefinedsymbollet s: symbol Symbol(id);表示唯一标识符bigintlet big: bigint 123n;用于超大整数这里有个特别容易忽略的点null和undefined在默认情况下可以赋值给任何类型。这句话是什么意思也就是说你写let name: string null;在不开strictNullChecks的时候是合法的。很多老项目里就是这么写过来的导致运行时经常出现Cannot read property of null。新项目我强烈建议你把strict模式打开让null和undefined老老实实待在自己的类型里。再说number类型不要以为它只表示“整数”。JavaScript 里只有一种数字类型也就是 IEEE 754 双精度浮点数所以 TypeScript 的number覆盖了整数、小数、NaN、Infinity这些。你写let num: number 1 / 0;也是合法的因为结果是Infinity。1.3 typeScript和JS的区别不仅仅是“写个类型”TypeScript 和 JavaScript 的最大区别不是语法上多了冒号和类型注解而是“类型系统”从无到有。具体的差异可以拆成下面这几点静态检查 vs 动态检查。JavaScript 在运行时才暴露类型问题TypeScript 在编译阶段就做检查。TypeScript 是 JavaScript 的超集。任何合法 JS 代码理论上都可以当作 TS 代码运行只是可能因为类型缺失而出现类型错误。类型注解是“可选的”。你不写类型TypeScript 会进行类型推断写了类型就多一层约束。运行前会被编译成 JavaScript类型信息会被删除不会影响运行时性能。很多初学者会纠结那我是不是要把所有变量都写上类型其实不用。TypeScript 有很聪明的类型推断机制比如let count 42;它会自动推断成number。你只需要在函数参数、函数返回值、对象结构这些“边界”位置显式标注类型中间的部分让推断去干活。这个设计的好处是你从 JS 迁移到 TS 的时候可以先保留大部分 JS 代码只给关键入口加上类型再逐步收紧不用一夜之间全部改造完。2. 数组、元组与枚举数据结构里的类型细节2.1 数组类型从 number[] 到 ReadonlyArray数组是每个前端项目里都躲不开的数据结构。在 TypeScript 里声明数组类型有两种等价写法number[]和Arraynumber。我喜欢用number[]读起来更自然但写泛型风格Arraynumber在某些场景下更统一比如Arraystring | null两种写法都没毛病选一种保持一致就好。除了基础写法还要知道readonly修饰符。一个只读数组readonly number[]表示这个数组本身不能增删改元素。你别小看这个约束很多数据源自外部接口你希望保证它在组件内部不被意外修改那就用ReadonlyArrayT或者readonly T[]来声明。实际项目中最容易出错的数组类型场景是“数组里混合了不同类型的值”。比如后端返回的数据可能是[1, 2, 3]这时候用(string | number)[]表示而不是直接用any[]。用any[]确实省事但等于把类型检查全扔了后面代码里做什么操作都不会有提示这很危险。2.2 元组类型数量和顺序也是类型的一部分元组Tuple是 TypeScript 里一个很有意思的类型它允许你声明一个固定长度、固定元素顺序和类型的数组。比如[string, number]意思就是第一个元素必须是字符串第二个元素必须是数字顺序不能反。为什么需要元组典型场景是处理 CSV、坐标数据、函数返回多个值时。比如一个函数返回[错误信息, 数据]你就可以声明成[string, T]这样的元组调用方取值时就能明确知道第一个值是错误信息第二个是数据。元组也有几个进阶用法可选元素比如[string, number?]剩余元素比如[string, ...number[]]表示第一个是字符串后面可以接任意多个数字。这些在复杂数据处理里非常有用但说实话日常业务代码里用得不多不过面试常问还是得掌握。一个常见的坑是元组类型并不是“完全不可变”的。let tuple: [string, number] [a, 1]; tuple.push(2);在 TypeScript 严格模式下可能会报错但在某些配置下push方法仍然存在因为数组方法不会因为元组的固定长度就消失。所以如果你要的是不可变元组记得加上readonly比如readonly [string, number]。2.3 枚举方便但必须知道编译后的样子枚举enum是 TypeScript 特有的JavaScript 里没有原生对应的东西。它可以让一组常量有更可读的名字比如enum Direction { Up, Down, Left, Right, }默认情况下Up的值是 0Down是 1依次递增。你还可以指定字符串值enum Color { Red RED, Green GREEN, Blue BLUE, }枚举很好用但它有一个需要特别注意的问题编译后的代码并不是简单地把枚举替换成值而是会生成一个对象。数字枚举还会生成反向映射也就是Direction[0]能取到Up。这会导致打包体积变大并且有些工具库会比较排斥枚举。比如在 React 生态里的不少库官方类型定义都推荐使用“联合类型 常量对象”来替代枚举。如果项目里确实需要枚举建议优先使用字符串枚举因为它更明确反向映射带来的隐患也少一些。还要注意const enum的使用const enum会在编译时直接内联性能好但在使用 babel、isolatedModules 等工具时会有兼容性问题很多项目干脆禁用const enum。这个知识点可以放进你的“学习笔记”里等用到了再回来翻。3. 特殊类型any、unknown、void、never 与字面量类型3.1 any和unknown风险程度完全不同any是 TypeScript 的“万能逃生口”但也是最容易被滥用的类型。给一个变量标注any等同于告诉 TypeScript别检查我了我想怎么用就怎么用。这在你从 JavaScript 一点点迁移到 TypeScript 的时候很有用可以先把类型错误糊弄过去但长期保留一堆any就失去了用 TypeScript 的意义。unknown是比any安全得多的“未知类型”。它表示“我现在不知道这个值是什么”但它不允许你直接使用。比如let data: unknown fetchSomeData(); // data.name; // 报错Object is of type unknown你必须先通过类型收窄把unknown变成具体类型才能访问属性。如果一段数据的类型非常动态优先用unknown再用typeof、in等操作符收窄这样在运行时和编译时都有安全感。一句话总结any是放弃类型检查unknown是保留类型检查但要你“先证明自己的判断”。3.2 void与never函数返回值的两种“空”void常见于声明没有返回值的函数function log(message: string): void { console.log(message); }注意void不表示“返回 undefined”而是“这里的返回值没有意义”。你可以返回undefined也可以不返回类型上都是允许的。never则更特殊它表示“永不返回值”。什么情况下是“永不”函数内部抛出异常或者进入死循环或者程序直接退出。比如function throwError(message: string): never { throw new Error(message); }never还有一个作用它是所有类型的子类型所以你可以用它来检查穷尽性。在switch语句里如果加了 default 分支并把变量断言成neverTypeScript 就能在你漏掉某种联合类型情况时发出编译错误。这个技巧在写复杂数据状态机时特别有用。3.3 字面量类型与const的暧昧关系字面量类型指的是把一个具体的值当作类型来用。比如up、42、true都可以作为类型let direction: up up;但如果你写let direction up;TypeScript 会把direction推断成string而不是up。为什么因为let声明的变量可以被重新赋值所以 TypeScript 会放宽成string。而const声明的变量不能重新赋值所以const direction up;会被推断成字面量类型up。这个差异在实际写配置对象时非常关键。比如const config { mode: dark, }; // config.mode 的类型是 string而不是 dark如果后面有个函数要求参数必须是dark | light你把config.mode传进去就会报错。解决办法是使用as const断言const config { mode: dark, } as const; // config.mode 的类型是 darkas const很常用但很多人不理解它到底做了什么其实就是把对象里所有属性都“冻结”成字面量类型数组也会变成只读元组。基础类型如果掌握到这里再看类型系统就会顺畅很多。4. 联合类型、类型收窄与类型推断实战中最常用4.1 联合类型是什么怎么用联合类型用|连接多个类型表示“这个值可以是这些类型中的任意一个”。比如let id: string | number; id abc123; id 123;联合类型很强大但有一个关键点你不能直接调用所有类型的共有方法之外的方法。比如string | number类型的变量你直接调用toUpperCase()会报错因为number上没有这个方法。你必须先做类型收窄。这个限制不是 TypeScript 故意刁难你而是它站在“运行时可能出错”的角度提醒你在没确认具体类型之前不能假设值一定能调用某个方法。这种思维和 JavaScript 的“写完再说”完全不同但习惯了之后你能在早期避免大量运行时错误。联合类型最经典的用法是“可空”场景string | null、User | undefined。配合strictNullChecks你写代码的时候会更容易意识到某个值可能为空然后主动判断。4.2 类型收窄把宽泛范围缩小到精确判断类型收窄指的是通过某种判断让 TypeScript 在代码分支里自动缩小类型范围。常见方式有以下几种typeof判断原始类型typeof value stringinstanceof判断类实例value instanceof Datein判断对象属性name in value数组判断Array.isArray(value)自定义类型守卫function isUser(value: unknown): value is User { ... }举个例子function handleInput(input: string | number) { if (typeof input string) { console.log(input.toUpperCase()); } else { console.log(input.toFixed(2)); } }在if分支里TypeScript 知道input一定被收窄成了string或number因此可以安全调用对应方法。类型收窄的核心是“让数据的可能性随着代码执行逐步收敛”。这比写一堆!非空断言靠谱多了因为非空断言本质上是你自己在拍胸脯“相信我这里一定不是空”。但如果判断逻辑错了运行时还是照样崩。能用收窄就用收窄。4.3 类型推断不写注解时TypeScript在做什么有时候你不写类型TypeScript 也会根据赋值内容推断出类型。如果你写let count 42;它推断成number如果你写const msg hello;它推断成hello这在前面的字面量类型里说过。更复杂一点的是对象和数组的推断。比如const user { name: 张三, age: 18 };TypeScript 会推断出{ name: string; age: number }这个对象类型。当你把对象传进函数时它会检查属性是否兼容。类型推断也有“过度推断”的坑。比如const arr [1, 2, 3];TypeScript 会推断成(string | number)[]这是符合逻辑的。但如果你希望它固定成[number, number, string]就需要显式声明元组类型或者用as const。在工作中我的习惯是能推断的就不写不能推断的、容易因为赋值而变化的边界才写。这样代码读起来更清爽也不会让类型注解变成噪音。5. 实操过程中容易踩的坑与排查方法5.1 tsconfig里的baseUrl弃用提示是啥情况最近很多人打开编辑器的时候会看到一条编译提示Option baseUrl is deprecated and will stop functioning in TypeScript 7.0.这说的就是tsconfig.json里的baseUrl配置。以前很多项目习惯用baseUrl配合paths做模块别名比如{ compilerOptions: { baseUrl: ., paths: { /*: [src/*] } } }这样就能用import utils from /utils这类写法。但 TypeScript 官方发现baseUrl经常被误用而且现代编译器在解析路径时不需要它也能工作于是决定在 7.0 里彻底移除这个选项。如果你项目里现在还在用baseUrl建议尽早处理。处理方式很简单把baseUrl删掉paths里的路径改成都指向tsconfig.json所在目录的相对路径{ compilerOptions: { paths: { /*: [./src/*] } } }这样行为基本不变同时也不会再收到弃用提示。别看这个点很小真到了 TypeScript 新版发布那天项目构建突然报错再去排查会浪费很多时间。5.2 类型断言 vs 类型守卫别一遇到报错就as类型断言有三种常见写法const a value as string; const b stringvalue; // 在 JSX 中不推荐 const c value!; // 非空断言类型断言是要谨慎使用的。它相当于你告诉 TypeScript“我知道这个值的类型不用你管了。” 但如果你判断错了编译时可能不报错运行时却会崩。比如const data: unknown getData(); const name (data as { name: string }).name;如果data是 null这行代码会直接抛错误。而用类型守卫的话至少能做一个运行时判断if (data typeof data object name in data) { const name data.name; }因此我建议类型断言用于“你比 TypeScript 更清楚类型”的场景比如明确知道接口返回字段但如果这个值的可靠性不确定优先用类型收窄。非空断言!也是同理能不用就不用用多了基本等于自己骗自己。5.3 对象类型interface、type alias和class怎么选对象是业务代码里最常见的类型载体TypeScript 里描述对象类型的方式有三种interface、type别名、class。interface适合描述对象的结构可以被extends继承也支持 declaration merging同名声明自动合并。type更灵活可以定义联合类型、元组、工具类型结果也可以通过做交叉类型。但它不能像interface那样重复声明合并。class除了描述结构还能提供实现逻辑、实例方法、访问修饰符。我的建议是对外 API 的对象结构优先用interface因为它有更好的错误提示和扩展能力内部复杂类型、联合类型、需要计算出来的类型用type只有当你需要封装实体类和面向对象编程时才用class。这里有一个很多人会遇到的疑惑为什么很多库的类型定义里type和interface都差不多其实两者在大多数情况下可以互换但你要明白各自的边界面试中也常被问到。你不用纠结谁更高级按场景选就行。6. 常见问题速查与学习路线建议6.1 常见报错速查表下面这张表是我在实际学习和答疑过程中经常遇到的报错按问题现象和解决方法整理出来可以直接保存当参考报错信息含义解决方法Type undefined is not assignable to type string变量可能为 undefined但类型上不接受打开 strictNullChecks显式处理 undefined 分支Property xxx does not exist on type yyy访问了类型上不存在的属性检查对象类型定义使用类型收窄或补属性Argument of type string is not assignable to parameter of type number参数类型不匹配检查函数签名中的类型确保传参类型正确Object is of type unknownunknown 类型不能直接访问先做类型断言或类型收窄Expected 2 arguments, but got 1函数缺少必填参数给参数设置默认值或把该参数变成可选参数Cannot find module /xxx or its corresponding type declarations模块路径别名解析失败检查 tsconfig paths 和 baseUrl确保路径配置和 tsconfig 相对位置正确Option baseUrl is deprecatedtsconfig 使用了即将弃用的 baseUrl删掉 baseUrl调整 paths 为相对路径遇到报错的时候不要急着复制错误信息去搜索引擎先自己读一遍错误信息。大多数 TypeScript 报错信息已经写得非常明确了哪个文件、哪一行、哪个类型不兼容基本都能定位到。6.2 学习基础类型的建议如果你现在刚开始学 TypeScript我建议按照下面这个路径来不要一上来就啃类型体操先把原始类型、数组、元组、枚举看完写几个小例子体会一下“类型约束”是什么感觉。然后在实际项目里用 TypeScript Playground 或本地项目把一个 JS 文件改成 TS 文件逐步加上类型。重点练习联合类型、类型收窄、字面量类型这三个是日常业务里每天都在用的。再学 interface 和 type 的区别学会怎么设计对象类型。最后再看泛型、工具类型、条件类型这些进阶内容。学习过程中官方文档依然是首选的参考资料中文社区的笔记也可以看但要学会分辨哪些是过时内容。比如网上很多教程还在教baseUrl这已经是弃用方案了。还有那些让你“把所有变量都加类型”的教程也不建议照做因为 TypeScript 的设计哲学是类型推断优先。我自己的体会是基础类型阶段最重要的是“练习把运行时的判断迁移到编译时”。你会慢慢发现自己写代码之前会先想一下这个数据的形状是什么这个函数可能收到什么类型。这种思维转变比记住一百个类型语法更有价值。最后再分享一个小技巧遇到不确定的类型先把它标成unknown然后强制自己用类型收窄去处理而不是直接as any。这个习惯能让你在不知不觉中提升对类型系统的掌控力。等你习惯了这种写法再看那些复杂的类型报错基本不会有畏难情绪了。