跳到主要内容

第 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 包提供两个可执行入口,都会用到:

命令作用
tscTypeScript 编译器:类型检查 + 编译成 JS
tsserverTypeScript 语言服务:编辑器用,通常不需要手动调

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 / bun

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 项目的事实标准配置,作用有两层:

  1. 告诉编译器检查什么include / exclude
  2. 告诉编译器怎么检查stricttargetmodule 等)

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 好类型的标准

写出「好类型」是个渐进的能力,先记三条标准:

  1. 精确:宁可 'pending' | 'done',也不要 string;宁可 { name: string },也不要 object
  2. 最少:只描述必要的结构,不给实现细节留余地
  3. 可读:别人一眼能看懂这个值能是什么

后续章节会反复围绕这三点打磨类型设计。


本章小结

  • 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