第 1 章 · 初识 TypeScript
- 理解 TypeScript 解决了什么问题、它「编译期做类型检查」的定位,以及它和 JavaScript 的关系
- 搭好最小工具链:Node、
tsc、编辑器,并会用--noEmit做类型检查 - 用三种方式运行 TypeScript 程序(编译产物 / 即时执行 / watch 模式)
- 读懂
tsconfig.json的核心字段,知道strict为什么必须开 - 会解读最常见的几类 TS 报错
- 理解类型推断与显式标注的分工:能推断就不写,标注只补盲区
1.1 为什么需要 TypeScript
1.1.1 动态类型的痛点
先看一段非常普通的 JavaScript:
function formatPrice(price) {
return `¥${price.toFixed(2)}`
}
formatPrice(19.9) // '¥19.90',一切正常
formatPrice('19.9') // 💥 TypeError: price.toFixed is not a function
问题出在调用方:formatPrice 期望一个数字,却传进了字符串。这段代码在运行到第二行之前完全「正常」,错误要等用户真正点开页面才会炸出来——而且炸在 price.toFixed 这种让人摸不着头脑的位置。
再举个例子,一个购物车对象:
const cart = { items: [], total: 0 }
// 某处代码把 total 写成了字符串
cart.total = '19.90'
// 另一处代码以为它是数字
const vat = cart.total * 0.13 // NaN,无人察觉
对象字段在 JavaScript 里没有「声明」,谁都能随手赋错类型,而且赋值时不报错、用的时候才出幺蛾子。项目一大,这种「数据契约被悄悄打破」的问题就成了 bug 的主要来源。
1.1.2 静态类型:把错误拦在运行之前
TypeScript 的做法是在运行之前就拦住它——只要给 price 标上类型:
function formatPrice(price: number) {
return `¥${price.toFixed(2)}`
}
formatPrice(19.9) // ✅
formatPrice('19.9') // ❌ 编译期就报错:类型 'string' 不能赋值给类型 'number'
TypeScript 不改变程序的运行时行为。它只在你写代码、编译的时候做类型检查;检查通过后,类型标注会被「擦掉」,最终运行的仍然是普通 JavaScript。换句话说:类型系统是开发期的保险丝,不是运行期的安全网。
同样,购物车的例子加上类型后,问题在写代码的瞬间就暴露:
interface Cart {
items: string[]
total: number
}
const cart: Cart = { items: [], total: 0 }
cart.total = '19.90' // ❌ 编译期报错:类型 'string' 不能赋值给类型 'number'
1.1.3 静态类型带来的四件套
| 价值 | 具体表现 |
|---|---|
| 提前发现错误 | 类型错误在编辑器里实时出现红波浪线,不用等运行时、不用等用户 |
| 编辑器体验 | 智能补全、跳转定义、查找引用、安全重命名,全都靠类型信息驱动 |
| 代码即文档 | 函数的签名就是它的说明书,团队协作时不用翻实现猜参数 |
| 大胆重构 | 改了接口/类型,所有用到它的地方立刻标红,不会悄悄漏改 |
尤其是重构这一点:在纯 JavaScript 里改一个字段名,你永远担心还有哪个角落没改到;有了类型,编译器会把你该改的地方全部列出来。
1.1.4 TypeScript 与 JavaScript 的关系
TypeScript 是 JavaScript 的超集:
┌───────────────────────────┐
│ JavaScript(合法 JS) │ ← 基本就是合法 TS
│ ┌─────────────────────┐ │
│ │ TypeScript 特有的类型│ │ ← 多出来的:类型标注、接口、泛型……
│ │ 标注、接口、泛型…… │ │
│ └─────────────────────┘ │
└───────────────────────────┘
- 把
.js文件改成.ts扩展名,往往能直接编译通过 - 你可以在一个文件里逐行迁移,JS 和 TS 可以共存
- TS 的语法是 ES 的超集,最新 ECMAScript 提案(甚至 TS 独有语法)都能写,再编译回目标版本
一句话:TS = JS + 类型系统。本书全程假设你已熟悉 JavaScript 语法,重点讲类型系统这一层。
1.2 安装与工具链
本书环境基于 TypeScript 6.0(见「版本说明」),Node.js 18+ 均可。
1.2.1 安装
# 初始化项目(已有 package.json 可跳过)
npm init -y
# 安装 TypeScript 作为开发依赖
npm i -D typescript
装完验证:
npx tsc --version
# 输出类似:Version 6.0.3
typescript 包提供两个可执行入口,都会用到:
| 命令 | 作用 |
|---|---|
tsc | TypeScript 编译器:类型检查 + 编译成 JS |
tsserver | TypeScript 语言服务:编辑器用,通常不需要手动调 |
1.2.2 编译器:tsc 的两种用法
用法一:编译出 JS 文件
npx tsc hello.ts
# 生成 hello.js(默认与源文件同目录)
适合「我要把 TS 编译成 JS 去运行/发布」的场景,但现代前端项目一般由打包器代劳。
用法二:只检查,不产出(主力用法)
npx tsc --noEmit hello.ts
--noEmit 的意思是只做类型检查,不生成任何 JS 文件。这是日常用得最多的姿势——你不需要产出 JS,只想知道「类型有没有问题」。
💡 在 CI(持续集成)里,
npx tsc --noEmit是极好的门禁:类型检查不过就构建失败。
1.2.3 运行 TypeScript 的三种方式
类型检查完,程序总得跑起来。按你的场景选:
| 方式 | 命令 | 适用 |
|---|---|---|
| 编译后运行 | npx tsc hello.ts && node hello.js | 最朴素,任何环境都行 |
| 即时执行 | npx tsx hello.ts | 本地开发跑 TS 文件,零配置(推荐) |
| 监视模式 | npx tsc --watch | 改一处编译一处,配合 node 做热更新 |
ts-node 是元老级的即时执行器,tsx(基于 esbuild)更快更稳,bun 则直接原生支持 TS。本书示例用 tsc --noEmit 做类型检查 + tsx 跑代码,两者职责清晰。你完全可以用你熟悉的组合。
1.2.4 编辑器:语言服务
编辑器体验由 TypeScript 的语言服务驱动,无需安装额外插件。VS Code(及 Trae 等兼容编辑器)打开 .ts 文件即可获得:
- 实时的红波浪线(类型错误)
- 悬停查看类型
- 智能补全(
Ctrl+Space) - 自动导入、跳转定义(
F12)、安全重命名(F2)
新手最好的习惯:写一行,悬停看一眼推断出的类型。这能帮你理解 TS 是怎么「想」的,比死记规则有效得多。
1.3 第一个 TypeScript 程序
1.3.1 一个带类型的小程序
下面这个程序综合了「类型标注」和「类型推断」,跑通它你就完成了第一课。新建 cart.ts:
// —— 类型标注:给函数参数和返回值写明类型 ——
// 计算两件商品的订单总额
function calcTotal(price: number, qty: number): number {
return price * qty
}
// 从数组中取出第一项;给空数组返回 undefined 而不是崩溃
function first<T>(arr: T[]): T | undefined {
return arr[0]
}
// —— 类型推断:下面的类型由 TS 自动推出 ——
const unitPrice = 19.9 // number
const cart = ['键盘', '鼠标'] // string[]
const total = calcTotal(unitPrice, cart.length) // number
console.log(first(cart)) // 推断返回 string | undefined
console.log(`合计:¥${total}`)
运行检查与执行:
npx tsc --noEmit cart.ts # 类型检查:无输出 = 通过
npx tsx cart.ts # 直接运行
第 1.3.1 节里出现的
first<T>是泛型,本书第 5 章会系统讲。现在只要知道「尖括号里的 T 是一个待填的类型占位」即可,先感受一下就好,看不懂也不影响往下读。
1.3.2 制造一个错误
把 cart.ts 里的一行改成错的:
const unitPrice: string = 19.9 // ❌ 19.9 是 number,不能给 string
tsc 立即报错:
error TS2322: Type 'number' is not assignable to type 'string'.
TS 报错的基本格式:error TS2322 是错误代号(可查),后面的英文是问题描述,箭头指向出错的代码位置。先读描述(Type 'number' is not assignable to type 'string'),再定位位置。看多了你会发现,绝大多数报错都属于那几类「套路」(见 1.3.3)。
1.3.3 五类最常见的报错
| 报错 | 含义 | 典型触发 |
|---|---|---|
Cannot find name 'xxx' | 变量/函数没定义(或没导入) | 拼错名字、忘了 import |
Type 'A' is not assignable to type 'B' | 类型不匹配 | 给 number 变量赋 string |
Property 'x' does not exist on type 'T' | 访问了类型上不存在的属性 | 打错属性名、对象类型没声明该字段 |
'x' is possibly 'undefined' / 'null' | 可能为空,需要先判空 | strict 下访问 T | undefined 的属性 |
Argument of type 'A' is not assignable to parameter of type 'B' | 实参类型不符函数形参 | 传参时类型对不上 |
遇到陌生报错,先把错误代号 + 关键类型复制到搜索框,绝大多数都有现成解答。报错不可怕,可怕的是被报错吓住不敢写——写类型报错是常态,学会读它才是入门的关键一步。
1.4 tsconfig.json
1.4.1 为什么需要它
类型检查的行为完全由 tsconfig.json 决定。没有它,tsc 会用默认值——这通常不是你要的。生成一份:
npx tsc --init
tsconfig.json 是 TS 项目的事实标准配置,作用有两层:
- 告诉编译器检查什么(
include/exclude) - 告诉编译器怎么检查(
strict、target、module等)
1.4.2 核心字段逐个看
{
"compilerOptions": {
// 编译产物的 ES 版本。es2022 是个稳妥选择
"target": "es2022",
// 模块系统。交给打包器的项目用 esnext 即可
"module": "esnext",
// 模块解析策略:bundler 适配现代打包器,node16/nodenext 适配 Node 项目
"moduleResolution": "bundler",
// 用到的环境类型(DOM、ES 新特性等)
"lib": ["es2023", "dom"],
// 编译输出目录(真正编译时才生效)
"outDir": "dist",
// 只做类型检查,不产出 JS —— 现代项目的主力姿势
"noEmit": true,
// ★ 严格模式全家桶:强烈建议保持 true
"strict": true
},
// 纳入检查的文件
"include": ["src"],
// 排除的目录(node_modules 一般会被自动排除)
"exclude": ["node_modules"]
}
几个字段的快速说明:
| 字段 | 说明 |
|---|---|
target | 决定把 ES 新语法「降级」到什么版本。写 es2022,箭头函数/可选链这类语法不会被转换 |
module / moduleResolution | 决定 import/require 怎么写、怎么解析。前端项目主流是 esnext + bundler |
lib | 声明环境里有哪些全局类型。浏览器代码要加 dom,纯 Node 不用 |
outDir | 只有真正编译时才需要。--noEmit 模式下无效 |
include | 只检查这里列出的目录,比在命令行逐个传文件省心得多 |
1.4.3 strict:最不该关的一个开关
strict: true 并不是单个开关,而是一系列严格检查的打包开关,包括:
| 子开关 | 作用 | 典型例子 |
|---|---|---|
strictNullChecks | 不把 null/undefined 当「一切类型的子类型」 | 默认情况下 null 不能赋给 number |
noImplicitAny | 禁止隐式的 any | 没标类型、又推断不出来的参数会报错 |
strictFunctionTypes | 函数参数逆变检查 | 回调参数类型不符会报错 |
| 以及更多 | …… | 详见第 12 章 |
看一个直观对比:
// strict: false —— 悄悄吞掉空值,运行期炸
function getLength(s) {
return s.length
}
getLength(null) // 💥 TypeError
// strict: true —— 编译期就告诉你问题
function getLength(s: string) {
return s.length
}
getLength(null) // ❌ 实参类型 'null' 不能赋值给形参类型 'string'
新项目请务必从 strict: true 开始。 从 false 起步的项目,后期要开启 strict 会收获海量报错,非常痛苦;而一路 strict 写下来,类型系统才能真正帮到你。第 12 章会系统讲 strict 全家桶。
1.4.4 查配置:认准官方文档
tsconfig 选项非常多,不用背。需要时查:
- 官方文档:TypeScript 配置参考
- 编辑器里把鼠标悬停在
tsconfig.json的字段上,VS Code 会给出说明
1.5 类型推断与显式标注
1.5.1 能推断就不写
TypeScript 绝非要求你标注每一个类型。绝大多数场景它能自己推断:
let age = 30 // number
const names = ['ada', 'bob'] // string[]
const user = { name: 'ada', age: 30 } // { name: string; age: number }
function add(a: number, b: number) {
return a + b // 返回类型推断为 number,不用写
}
核心原则:能推断就不写,标注只补盲区。 到处写类型不仅啰嗦,还会让代码失去可读性。
1.5.2 什么时候必须显式标注
下面四类场景,TS 自己看不出来,需要你出手:
// 1. 函数参数:调用方传什么,TS 猜不到
function greet(name: string) { ... }
// 2. 空数组/空对象:初始为空,后续会填不同类型
const items: string[] = []
const config: Record<string, number> = {}
// 3. 函数的返回类型:希望它「固定」下来,不随实现漂移
function parse(input: string): number { ... }
// 4. 跨文件/跨模块的边界:导出给外部用的函数
export function fetchUser(id: number): Promise<User> { ... }
一个典型的「该标注却没标注」的例子——参数不写类型,strict 会报隐式 any:
function logMessage(msg) { // ❌ 参数 'msg' 隐式具有 'any' 类型
console.log(msg.toUpperCase())
}
any 是类型系统的「黑洞」,它会悄悄关闭该处的类型检查(第 2 章细讲)。能用具体类型就别用 any。
1.5.3 好类型的标准
写出「好类型」是个渐进的能力,先记三条标准:
- 精确:宁可
'pending' | 'done',也不要string;宁可{ name: string },也不要object - 最少:只描述必要的结构,不给实现细节留余地
- 可读:别人一眼能看懂这个值能是什么
后续章节会反复围绕这三点打磨类型设计。
本章小结
- TypeScript 在编译期做类型检查,把运行时错误提前到开发期拦截;它不改变运行时行为
- 最小工具链:
npm i -D typescript+npx tsc --noEmit(只检查)+npx tsx(跑起来) - 编辑器体验由语言服务驱动,悬停看类型是好习惯
- 常见报错有套路,学会读「错误代号 + 类型描述」是关键
tsconfig.json控制检查行为,strict: true是新项目的底线- 类型推断优先,显式标注只补盲区;好类型要精确、最少、可读
本章练习
挑战题(type-challenges 00013 · Hello World)
在 挑战题库 点「开始挑战」,在官方 Playground 里把:
type HelloWorld = any // expected to be a string
改成让 HelloWorld 精确等于 string。编译零错误即通过——这是你类型体操之路的第一小步。🏃
本书的练习来自 type-challenges,训练的是类型层面的编程能力。它和「用 TS 写业务代码」互补:业务代码靠本章这套工具链,类型体操则把类型系统本身当成要操作的对象。后者的功底会在第 6、7 章之后爆发式地反哺前者。
代码练习
练习 1 · strict 开关对比
新建项目,在 strict: false 下写一个「传错类型也不报错」的函数(比如参数不标类型、传 null 进去),再切到 strict: true,逐一观察新增的报错并记录。这是理解 strict 价值最直观的方式。
练习 2 · 给 JS 补类型
为下面这段无类型代码补上类型标注,让 strict 模式通过:
// 期望:amount 是 number、name 是 string、tags 是 string[]
// 期望:返回对象额外多一个 number 类型的 id
function createProduct(data) {
return { id: Math.random(), ...data }
}
createProduct({ amount: 3, name: '咖啡', tags: ['热', '大杯'] })
练习 3 · 搭好你的构建脚本
在 package.json 里加一个 typecheck 脚本,让 npm run typecheck 等价于对全项目跑 tsc --noEmit。后续章节所有练习都在这套配置下进行:
{
"scripts": {
"typecheck": "tsc --noEmit"
}
}
练习 4 · 解读三个报错
故意写下这三行错代码,用 tsc --noEmit 编译,用自己的话解释每一条报错:
const count: number = '12' // 报错 1
function f(x: number) { return x + 1 }
f(true) // 报错 2
const obj = { name: 'ada' }
obj.age // 报错 3