news 2026/9/9 16:38:23

TypeScript 基础类型详解:从原理到实战的必备指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
TypeScript 基础类型详解:从原理到实战的必备指南

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 / false
numberlet count: number = 42;整数、浮点数、NaN、Infinity 都算
stringlet name: string = 'typescript';单引号、双引号、模板字符串都行
nulllet n: null = null;只能赋值为 null
undefinedlet u: undefined = undefined;只能赋值为 undefined
symbollet s: symbol = Symbol('id');表示唯一标识符
bigintlet big: bigint = 123n;用于超大整数

这里有个特别容易忽略的点:nullundefined在默认情况下,可以赋值给任何类型。这句话是什么意思?也就是说你写let name: string = null;,在不开strictNullChecks的时候是合法的。很多老项目里就是这么写过来的,导致运行时经常出现Cannot read property of null。新项目我强烈建议你把strict模式打开,让nullundefined老老实实待在自己的类型里。

再说number类型,不要以为它只表示“整数”。JavaScript 里只有一种数字类型,也就是 IEEE 754 双精度浮点数,所以 TypeScript 的number覆盖了整数、小数、NaNInfinity这些。你写let num: number = 1 / 0;也是合法的,因为结果是Infinity

1.3 typeScript和JS的区别:不仅仅是“写个类型”

TypeScript 和 JavaScript 的最大区别,不是语法上多了冒号和类型注解,而是“类型系统”从无到有。具体的差异可以拆成下面这几点:

  1. 静态检查 vs 动态检查。JavaScript 在运行时才暴露类型问题,TypeScript 在编译阶段就做检查。
  2. TypeScript 是 JavaScript 的超集。任何合法 JS 代码理论上都可以当作 TS 代码运行,只是可能因为类型缺失而出现类型错误。
  3. 类型注解是“可选的”。你不写类型,TypeScript 会进行类型推断;写了类型,就多一层约束。
  4. 运行前会被编译成 JavaScript,类型信息会被删除,不会影响运行时性能。

很多初学者会纠结:那我是不是要把所有变量都写上类型?其实不用。TypeScript 有很聪明的类型推断机制,比如let count = 42;它会自动推断成number。你只需要在函数参数、函数返回值、对象结构这些“边界”位置显式标注类型,中间的部分让推断去干活。

这个设计的好处是:你从 JS 迁移到 TS 的时候,可以先保留大部分 JS 代码,只给关键入口加上类型,再逐步收紧,不用一夜之间全部改造完。

2. 数组、元组与枚举:数据结构里的类型细节

2.1 数组类型:从 number[] 到 ReadonlyArray

数组是每个前端项目里都躲不开的数据结构。在 TypeScript 里,声明数组类型有两种等价写法:number[]Array<number>。我喜欢用number[],读起来更自然;但写泛型风格Array<number>在某些场景下更统一,比如Array<string | null>,两种写法都没毛病,选一种保持一致就好。

除了基础写法,还要知道readonly修饰符。一个只读数组readonly number[]表示这个数组本身不能增删改元素。你别小看这个约束,很多数据源自外部接口,你希望保证它在组件内部不被意外修改,那就用ReadonlyArray<T>或者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的值是 0,Down是 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,再用typeofin等操作符收窄,这样在运行时和编译时都有安全感。

一句话总结: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 分支并把变量断言成never,TypeScript 就能在你漏掉某种联合类型情况时发出编译错误。这个技巧在写复杂数据状态机时特别有用。

3.3 字面量类型与const的暧昧关系

字面量类型指的是把一个具体的值当作类型来用。比如'up'42true都可以作为类型:

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 的类型是 'dark'

as const很常用,但很多人不理解它到底做了什么,其实就是把对象里所有属性都“冻结”成字面量类型,数组也会变成只读元组。基础类型如果掌握到这里,再看类型系统就会顺畅很多。

4. 联合类型、类型收窄与类型推断:实战中最常用

4.1 联合类型是什么,怎么用

联合类型用|连接多个类型,表示“这个值可以是这些类型中的任意一个”。比如:

let id: string | number; id = 'abc123'; id = 123;

联合类型很强大,但有一个关键点:你不能直接调用所有类型的共有方法之外的方法。比如string | number类型的变量,你直接调用toUpperCase()会报错,因为number上没有这个方法。你必须先做类型收窄。

这个限制不是 TypeScript 故意刁难你,而是它站在“运行时可能出错”的角度提醒你:在没确认具体类型之前,不能假设值一定能调用某个方法。这种思维和 JavaScript 的“写完再说”完全不同,但习惯了之后,你能在早期避免大量运行时错误。

联合类型最经典的用法是“可空”场景:string | nullUser | undefined。配合strictNullChecks,你写代码的时候会更容易意识到某个值可能为空,然后主动判断。

4.2 类型收窄:把宽泛范围缩小到精确判断

类型收窄指的是通过某种判断,让 TypeScript 在代码分支里自动缩小类型范围。常见方式有以下几种:

  • typeof判断原始类型:typeof value === 'string'
  • instanceof判断类实例:value instanceof Date
  • in判断对象属性:'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一定被收窄成了stringnumber,因此可以安全调用对应方法。

类型收窄的核心是“让数据的可能性随着代码执行逐步收敛”。这比写一堆!非空断言靠谱多了,因为非空断言本质上是你自己在拍胸脯:“相信我,这里一定不是空”。但如果判断逻辑错了,运行时还是照样崩。能用收窄就用收窄。

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 = <string>value; // 在 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 里描述对象类型的方式有三种:interfacetype别名、class

  • interface:适合描述对象的结构,可以被extends继承,也支持 declaration merging(同名声明自动合并)。
  • type:更灵活,可以定义联合类型、元组、工具类型结果,也可以通过&做交叉类型。但它不能像interface那样重复声明合并。
  • class:除了描述结构,还能提供实现逻辑、实例方法、访问修饰符。

我的建议是:对外 API 的对象结构优先用interface,因为它有更好的错误提示和扩展能力;内部复杂类型、联合类型、需要计算出来的类型,用type;只有当你需要封装实体类和面向对象编程时,才用class

这里有一个很多人会遇到的疑惑:为什么很多库的类型定义里,typeinterface都差不多?其实两者在大多数情况下可以互换,但你要明白各自的边界,面试中也常被问到。你不用纠结谁更高级,按场景选就行。

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 'unknown'unknown 类型不能直接访问先做类型断言或类型收窄
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,我建议按照下面这个路径来,不要一上来就啃类型体操:

  1. 先把原始类型、数组、元组、枚举看完,写几个小例子,体会一下“类型约束”是什么感觉。
  2. 然后在实际项目里用 TypeScript Playground 或本地项目,把一个 JS 文件改成 TS 文件,逐步加上类型。
  3. 重点练习联合类型、类型收窄、字面量类型,这三个是日常业务里每天都在用的。
  4. 再学 interface 和 type 的区别,学会怎么设计对象类型。
  5. 最后再看泛型、工具类型、条件类型这些进阶内容。

学习过程中,官方文档依然是首选的参考资料,中文社区的笔记也可以看,但要学会分辨哪些是过时内容。比如网上很多教程还在教baseUrl,这已经是弃用方案了。还有那些让你“把所有变量都加类型”的教程,也不建议照做,因为 TypeScript 的设计哲学是类型推断优先。

我自己的体会是,基础类型阶段最重要的是“练习把运行时的判断迁移到编译时”。你会慢慢发现自己写代码之前,会先想一下这个数据的形状是什么,这个函数可能收到什么类型。这种思维转变,比记住一百个类型语法更有价值。

最后再分享一个小技巧:遇到不确定的类型,先把它标成unknown,然后强制自己用类型收窄去处理,而不是直接as any。这个习惯能让你在不知不觉中提升对类型系统的掌控力。等你习惯了这种写法,再看那些复杂的类型报错,基本不会有畏难情绪了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 16:37:52

4.14 也能 root?KernelSU 旧内核适配实战,老设备先别划走

4.14 也能 root&#xff1f;KernelSU 旧内核适配实战&#xff0c;老设备先别划走 【免费下载链接】KernelSU A Kernel based root solution for Android 项目地址: https://gitcode.com/GitHub_Trending/ke/KernelSU 你的设备内核太老&#xff0c;装 KernelSU 直接显示 …

作者头像 李华
网站建设 2026/9/9 16:31:41

JAVA毕设选题推荐:基于SpringBoot+Vue的医患在线问诊交互平台的设计与实现 基于SpringBoot+Vue的智慧门诊问诊服【附源码、mysql、文档、调试+代码讲解+全bao等】

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

作者头像 李华
网站建设 2026/9/9 16:31:17

WorkBuddy保姆级教程:AI Agent让办公自动化从想法到落地

之前在帮团队推 AI 办公落地时&#xff0c;我发现大部分人的卡点不是“不会用 AI”&#xff0c;而是“AI 只会聊&#xff0c;不会干活”。让模型写一段文案没问题&#xff0c;但让它自动整理附件、按固定模板生成周报、把零散信息拆成待办清单&#xff0c;普通对话式助手就很容…

作者头像 李华