跳到主要内容

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 提供了 stringnumber 等语言原语,但不会检查你是否一致地使用这些类型。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 的核心优势

特性JavaScriptTypeScript
类型系统动态类型静态类型(可选)
类型检查运行时编译时
错误发现运行时发现编写时发现
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 已有一组基本类型:booleanbigintnullnumberstringsymbolundefined,你可以在接口中使用这些类型。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 类型描述为 truefalse

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

类型谓词
stringtypeof s === "string"
numbertypeof n === "number"
booleantypeof b === "boolean"
undefinedtypeof undefined === "undefined"
functiontypeof f === "function"
arrayArray.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 提供了两种定义类型的方式:interfacetype。你应该优先使用 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)

编译过程解析

  1. 类型检查:分析代码中的类型错误
  2. 类型擦除:移除所有类型注解
  3. 代码转换:将 TypeScript 语法转换为 JavaScript
  4. 生成输出:产生可执行的 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

学习路线

第一阶段:基础入门

  1. 环境配置 - 安装 TypeScript 和配置开发环境
  2. 基础类型 - 学习 TypeScript 的基本类型
  3. 接口 - 定义对象的形状
  4. 函数 - 函数类型和重载
  5. - 面向对象编程

第二阶段:进阶特性

  1. 泛型 - 创建可复用的组件
  2. 模块 - 模块化代码组织
  3. 高级类型 - 联合类型、交叉类型、条件类型
  4. 装饰器 - 元编程和注解

第三阶段:工程实践

  1. 声明文件 - 为 JavaScript 库添加类型
  2. 配置详解 - tsconfig.json 配置选项
  3. React 开发 - TypeScript + React 最佳实践
  4. Node.js 开发 - TypeScript + Node.js 最佳实践

第四阶段:测试与质量

  1. 测试 - TypeScript 项目测试策略

参考资料

学习建议

前置知识

  • JavaScript 基础:TypeScript 是 JavaScript 的超集,需要先掌握 JavaScript
  • ES6+ 语法:箭头函数、解构、模块等现代 JavaScript 特性
  • 面向对象概念:类、继承、接口等概念有助于理解 TypeScript

学习策略

  1. 循序渐进:从简单类型开始,逐步学习高级特性
  2. 动手实践:每学一个知识点,都要编写代码验证
  3. 阅读错误:TypeScript 的错误信息很详细,学会阅读和理解
  4. 利用 IDE:使用 VS Code 等支持 TypeScript 的编辑器
  5. 查看源码:阅读流行库的类型定义文件

渐进式采用

TypeScript 设计之初就考虑了渐进式采用。你可以:

// 1. 允许 JS 文件
// tsconfig.json: { "allowJs": true }

// 2. 从宽松模式开始
// tsconfig.json: { "strict": false }

// 3. 逐步添加类型
// any -> unknown -> 具体类型

// 4. 开启严格模式
// tsconfig.json: { "strict": true }

参考资源

官方资源

学习资源

准备好开始学习了吗?让我们从 环境配置 开始你的 TypeScript 之旅!