TypeScript 教程
欢迎学习 TypeScript!本教程将带你从零基础开始,系统掌握 TypeScript 的核心知识和实战技能。
本教程基于 TypeScript 5.7 编写,涵盖最新的语言特性。TypeScript 5.0 引入了装饰器标准化、const 类型参数、satisfies 操作符等重要特性,5.4 版本增强了类型 narrowing 能力,5.7 版本新增了 with 语句支持、unique symbol 改进、--target es2024 等特性。
什么是 TypeScript?
TypeScript 是由微软开发的开源编程语言,是 JavaScript 的严格超集。它在 JavaScript 基础上添加了可选的静态类型系统和基于类的面向对象编程。
TypeScript 与 JavaScript 的关系
TypeScript 与 JavaScript 保持着一种独特的关系:TypeScript 提供了 JavaScript 的所有功能,并在其之上添加了一层类型系统。
// 这段 JavaScript 代码
let message = "Hello World";
// 完全等价于这段 TypeScript 代码
let message = "Hello World";
JavaScript 提供了 string、number 等语言原语,但不会检查你是否一致地使用这些类型。TypeScript 会。
// JavaScript - 运行时才发现错误
function add(a, b) {
return a + b;
}
add(1, "2"); // 返回 "12",字符串拼接,而非数学加法
// TypeScript - 编译时发现错误
function add(a: number, b: number): number {
return a + b;
}
add(1, "2"); // 错误:参数 "2" 不能赋值给类型 "number"
关键洞察:这意味着你现有的可运行的 JavaScript 代码同时也是 TypeScript 代码。TypeScript 的主要优势是它可以突出代码中的意外行为,从而降低出错的可能性。
为什么需要 TypeScript?
1. 静态类型检查
JavaScript 是动态类型语言,变量的类型在运行时才能确定。这导致了两个问题:
- 错误发现延迟:类型错误只能在运行时发现
- IDE 支持有限:没有类型信息,代码补全和重构功能受限
// JavaScript 的痛点
const user = {
name: "张三",
age: 25
};
// 拼写错误不会被发现
console.log(user.nmae); // undefined,运行时才发现
// 类型错误不会被发现
user.age = "二十六"; // 年龄应该是数字,但 JavaScript 允许
// TypeScript 的解决方案
interface User {
name: string;
age: number;
}
const user: User = {
name: "张三",
age: 25
};
// 编译时报错:属性 'nmae' 不存在
console.log(user.nmae); // 错误!
// 编译时报错:不能将 string 赋给 number
user.age = "二十六"; // 错误!
2. 更好的开发体验
TypeScript 的类型信息为编辑器提供了丰富的上下文,带来以下优势:
- 智能补全:精确的代码补全建议
- 即时反馈:编写代码时就能看到错误
- 安全重构:重命名、移动文件时自动更新所有引用
- 内联文档:悬停查看类型定义和文档注释
3. 大型项目支持
TypeScript 特别适合团队协作和大型项目:
- 接口定义即文档,降低沟通成本
- 类型约束防止意外修改
- 更容易进行代码审查
- 新成员更容易理解代码库
TypeScript 的核心优势
| 特性 | JavaScript | TypeScript |
|---|---|---|
| 类型系统 | 动态类型 | 静态类型(可选) |
| 类型检查 | 运行时 | 编译时 |
| 错误发现 | 运行时发现 | 编写时发现 |
| IDE 支持 | 有限 | 强大的智能提示和重构 |
| 学习曲线 | 较低 | 需要学习类型概念 |
| 代码维护 | 随项目增大变难 | 大型项目表现优秀 |
| 渐进采用 | 不适用 | 可以逐步迁移 |
类型系统核心概念
理解 TypeScript 的类型系统是掌握 TypeScript 的关键。以下是几个核心概念:
类型推断(Type Inference)
TypeScript 了解 JavaScript 语言,在许多情况下会自动为你生成类型。这就是类型推断——无需显式注解,TypeScript 就能推断出变量的类型。
// TypeScript 推断 helloWorld 是 string 类型
let helloWorld = "Hello World";
// 等价于显式注解
let helloWorld: string = "Hello World";
通过理解 JavaScript 的工作方式,TypeScript 构建了一个接受 JavaScript 代码但具有类型的类型系统。这提供了类型系统的优势,而无需在代码中添加额外字符来显式指定类型。
类型推断的常见场景:
// 变量初始化
let count = 10; // number
let names = ["a", "b"]; // string[]
// 函数返回值
function add(a: number, b: number) {
return a + b; // 返回类型推断为 number
}
// 对象属性
const user = {
name: "张三", // string
age: 25 // number
};
定义类型
虽然类型推断很强大,但 JavaScript 的某些设计模式使类型难以自动推断(例如使用动态编程的模式)。为了覆盖这些情况,TypeScript 支持扩展 JavaScript 语言,提供指定类型的方式。
使用接口定义对象形状
// 定义 User 接口
interface User {
name: string;
id: number;
}
// 使用接口注解变量
const user: User = {
name: "Hayes",
id: 0
};
如果提供的对象与接口不匹配,TypeScript 会发出警告:
interface User {
name: string;
id: number;
}
const user: User = {
username: "Hayes", // 错误:'username' 不存在于类型 'User'
id: 0
};
接口与类
由于 JavaScript 支持类和面向对象编程,TypeScript 也支持。你可以在类中使用接口:
interface User {
name: string;
id: number;
}
class UserAccount {
name: string;
id: number;
constructor(name: string, id: number) {
this.name = name;
this.id = id;
}
}
const user: User = new UserAccount("Murphy", 1);
你还可以使用接口来注解函数的参数和返回值:
function deleteUser(user: User) {
// ...
}
function getAdminUser(): User {
// ...
}
TypeScript 特有类型
JavaScript 已有一组基本类型:boolean、bigint、null、number、string、symbol 和 undefined,你可以在接口中使用这些类型。TypeScript 扩展了这个列表,添加了更多类型:
any:允许任何类型,关闭类型检查unknown:确保使用此类型的人必须声明类型是什么never:表示不可能发生的类型void:表示函数返回undefined或没有返回值
// any - 放弃类型检查
let anything: any = 4;
anything = "hello"; // OK
anything = true; // OK
// unknown - 安全的 any
let unsure: unknown = 4;
unsure = "hello"; // OK
// 使用前必须进行类型检查
if (typeof unsure === "string") {
console.log(unsure.toUpperCase()); // OK
}
// never - 不可能到达
function fail(message: string): never {
throw new Error(message);
}
// void - 无返回值
function log(message: string): void {
console.log(message);
}
组合类型
TypeScript 允许通过组合简单类型来创建复杂类型。有两种流行的方式:联合类型和泛型。
联合类型
使用联合类型,你可以声明一个类型可以是多种类型之一。例如,你可以将 boolean 类型描述为 true 或 false:
type MyBool = true | false;
注意:如果你将鼠标悬停在上面的 MyBool 上,你会看到它被归类为 boolean。这是结构类型系统的特性,后面会详细讨论。
联合类型的一个常见用途是描述允许的字符串或数字字面量集合:
type WindowStates = "open" | "closed" | "minimized";
type LockStates = "locked" | "unlocked";
type PositiveOddNumbersUnderTen = 1 | 3 | 5 | 7 | 9;
联合类型也提供了处理不同类型的方式:
function getLength(obj: string | string[]) {
return obj.length;
}
要了解变量的类型,可以使用 typeof:
| 类型 | 谓词 |
|---|---|
| string | typeof s === "string" |
| number | typeof n === "number" |
| boolean | typeof b === "boolean" |
| undefined | typeof undefined === "undefined" |
| function | typeof f === "function" |
| array | Array.isArray(a) |
例如,你可以让函数根据传入的是字符串还是数组返回不同的值:
function wrapInArray(obj: string | string[]) {
if (typeof obj === "string") {
return [obj];
}
return obj;
}
泛型
泛型为类型提供变量。一个常见的例子是数组。没有泛型的数组可以包含任何内容。有泛型的数组可以描述数组包含的值。
type StringArray = Array<string>;
type NumberArray = Array<number>;
type ObjectWithNameArray = Array<{ name: string }>;
你可以声明自己使用泛型的类型:
interface Backpack<Type> {
add: (obj: Type) => void;
get: () => Type;
}
// 声明存在一个名为 backpack 的常量
declare const backpack: Backpack<string>;
// object 是 string,因为上面声明了它是 Backpack 的变量部分
const object = backpack.get();
// 由于 backpack 变量是 string,不能传 number 给 add 函数
backpack.add(23); // 错误!
结构类型系统
TypeScript 的核心原则之一是类型检查关注值的形状。这有时被称为"鸭子类型"或"结构类型"。
在结构类型系统中,如果两个对象具有相同的形状,则认为它们属于同一类型。
interface Point {
x: number;
y: number;
}
function logPoint(p: Point) {
console.log(`${p.x}, ${p.y}`);
}
// 打印 "12, 26"
const point = { x: 12, y: 26 };
logPoint(point);
point 变量从未被声明为 Point 类型。但是,TypeScript 在类型检查中将 point 的形状与 Point 的形状进行比较。它们具有相同的形状,所以代码通过了。
形状匹配只要求对象字段的子集匹配:
const point3 = { x: 12, y: 26, z: 89 };
logPoint(point3); // 打印 "12, 26"
const rect = { x: 33, y: 3, width: 30, height: 80 };
logPoint(rect); // 打印 "33, 3"
const color = { hex: "#187ABF" };
logPoint(color); // 错误:缺少 x 和 y 属性
类和对象符合形状的方式没有区别:
class VirtualPoint {
x: number;
y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
const newVPoint = new VirtualPoint(13, 56);
logPoint(newVPoint); // 打印 "13, 56"
如果对象或类具有所有必需的属性,无论实现细节如何,TypeScript 都会说它们匹配。
interface 与 type 的选择
TypeScript 提供了两种定义类型的方式:interface 和 type。你应该优先使用 interface。当你需要特定功能时使用 type。
// 推荐使用 interface
interface User {
name: string;
id: number;
}
// type 适用于以下场景:
// 1. 联合类型
type ID = string | number;
// 2. 映射类型
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
// 3. 条件类型
type NonNullable<T> = T extends null | undefined ? never : T;
TypeScript 编译流程
TypeScript 代码需要先编译成 JavaScript 才能在浏览器或 Node.js 中运行:
TypeScript 源码 (.ts)
│
▼
TypeScript 编译器 (tsc)
│
▼
JavaScript 代码 (.js)
│
▼
JavaScript 运行时(浏览器/Node.js)
编译过程解析:
- 类型检查:分析代码中的类型错误
- 类型擦除:移除所有类型注解
- 代码转换:将 TypeScript 语法转换为 JavaScript
- 生成输出:产生可执行的 JavaScript 代码
// TypeScript 源码
function greet(name: string): string {
return `Hello, ${name}!`;
}
// 编译后的 JavaScript
function greet(name) {
return "Hello, ".concat(name, "!");
}
TypeScript 版本演进
TypeScript 5.x 新特性
TypeScript 5.0 是一个重要的里程碑版本,引入了多项重大改进:
装饰器(Decorators)
TypeScript 5.0 实现了即将到来的 ECMAScript 装饰器标准:
function loggedMethod(originalMethod: any, context: ClassMethodDecoratorContext) {
const methodName = String(context.name);
function replacementMethod(this: any, ...args: any[]) {
console.log(`调用 ${methodName}`);
const result = originalMethod.call(this, ...args);
console.log(`退出 ${methodName}`);
return result;
}
return replacementMethod;
}
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
@loggedMethod
greet() {
console.log(`你好,${this.name}!`);
}
}
const 类型参数
使用 const 修饰符获得更精确的类型推断:
// 之前:推断为 string[]
function getNamesExactly<T extends string>(names: T[]): T[] {
return names;
}
const names = getNamesExactly(["Alice", "Bob", "Eve"]); // string[]
// 现在:推断为 readonly ["Alice", "Bob", "Eve"]
function getNamesExactly<const T extends string>(names: T[]): T[] {
return names;
}
const names = getNamesExactly(["Alice", "Bob", "Eve"]);
// names 类型为 readonly ["Alice", "Bob", "Eve"]
satisfies 操作符(4.9 引入,5.0 增强)
验证表达式类型同时保留原始类型:
type Colors = Record<string, string | number[]>;
const palette = {
red: "#FF0000",
blue: [0, 0, 255],
green: "#00FF00"
} satisfies Colors;
// palette.red 类型是 string,不是 string | number[]
palette.red.toUpperCase(); // OK!
// palette.blue 类型是 number[]
palette.blue.push(128); // OK!
所有枚举都是联合枚举
TypeScript 5.0 让所有枚举都成为联合枚举,即使是计算成员:
enum E {
A = 10 * 10, // 计算成员
B = 20, // 字面量成员
}
// 现在可以正常工作
function f(x: E) {
if (x === E.A) {
// x 被收窄为 E.A
}
}
TypeScript 5.4 新特性
闭包中的类型收窄
TypeScript 5.4 改进了在闭包中收窄类型的能力:
function getLength(str: string | null) {
if (str !== null) {
// 之前:在回调中 str 的类型可能会重置
// 现在:str 在回调中仍然被收窄为 string
return str.length;
}
return 0;
}
NoInfer 工具类型
防止类型推断,强制显式指定类型:
function createCircle<T extends string>(color: T, radius: NoInfer<number>) {
return { color, radius };
}
// radius 必须显式传入数字,不会被推断
createCircle("red", 10); // OK
学习路线
第一阶段:基础入门
第二阶段:进阶特性
第三阶段:工程实践
- 声明文件 - 为 JavaScript 库添加类型
- 配置详解 - tsconfig.json 配置选项
- React 开发 - TypeScript + React 最佳实践
- Node.js 开发 - TypeScript + Node.js 最佳实践
第四阶段:测试与质量
- 测试 - TypeScript 项目测试策略
参考资料
- 知识速查 - TypeScript 常用语法速查
学习建议
前置知识
- JavaScript 基础:TypeScript 是 JavaScript 的超集,需要先掌握 JavaScript
- ES6+ 语法:箭头函数、解构、模块等现代 JavaScript 特性
- 面向对象概念:类、继承、接口等概念有助于理解 TypeScript
学习策略
- 循序渐进:从简单类型开始,逐步学习高级特性
- 动手实践:每学一个知识点,都要编写代码验证
- 阅读错误:TypeScript 的错误信息很详细,学会阅读和理解
- 利用 IDE:使用 VS Code 等支持 TypeScript 的编辑器
- 查看源码:阅读流行库的类型定义文件
渐进式采用
TypeScript 设计之初就考虑了渐进式采用。你可以:
// 1. 允许 JS 文件
// tsconfig.json: { "allowJs": true }
// 2. 从宽松模式开始
// tsconfig.json: { "strict": false }
// 3. 逐步添加类型
// any -> unknown -> 具体类型
// 4. 开启严格模式
// tsconfig.json: { "strict": true }
参考资源
官方资源
学习资源
- TypeScript Playground - 在线练习环境
- TypeScript Deep Dive - 深入理解 TypeScript
- type-challenges - 类型体操练习
准备好开始学习了吗?让我们从 环境配置 开始你的 TypeScript 之旅!