快速了解 TypeScript:中文基础介绍 – wiki基地


快速了解 TypeScript:中文基础介绍

引言:为什么我们需要 TypeScript?

JavaScript 是当今前端和后端开发领域不可或缺的语言。它的灵活性、动态性以及庞大的生态系统使其广受欢迎。然而,随着项目规模的扩大和团队成员的增加,JavaScript 的动态特性有时也会带来挑战:难以预测的类型错误、代码难以维护、大型重构风险高、缺乏强大的工具支持等问题逐渐浮现。

想象一下,你在开发一个大型应用程序,多人协作。某个函数期望接收一个数字作为参数,但由于缺乏明确的类型约束,其他开发者可能在不知情的情况下传入了一个字符串或一个对象,这直到运行时才会报错,而且定位问题往往需要花费不少时间。或者,你想重构一个类或接口的名称,但因为IDE无法准确知道哪些地方使用了这个类或接口,你不得不手动搜索替换,这既低效又容易出错。

正是在这样的背景下,TypeScript 应运而生。

TypeScript 是由微软开发和维护的一种开源编程语言。它是 JavaScript 的一个 超集 (Superset),这意味着任何合法的 JavaScript 代码都是合法的 TypeScript 代码。TypeScript 在 JavaScript 的基础上添加了 静态类型 (Static Typing) 和其他一些特性,如接口 (Interfaces)、泛型 (Generics)、枚举 (Enums) 等。最终,TypeScript 代码会被编译 (Compile) 成纯粹、干净的 JavaScript 代码,然后在任何支持 JavaScript 的地方运行(浏览器、Node.js 等)。

通过引入静态类型,TypeScript 可以在开发阶段(编译阶段)就捕获许多潜在的运行时错误,这极大地提高了代码的健壮性和可维护性。同时,它也为现代开发工具(如 VS Code、WebStorm 等)提供了丰富的信息,从而实现了强大的代码补全、导航、重构等功能,显著提升了开发效率。

本文将带你快速入门 TypeScript,详细介绍其基本概念、核心语法以及如何在项目中使用它。无论你是刚刚接触前端或后端开发,还是有一定 JavaScript 经验希望提升代码质量和开发效率的开发者,本文都将为你打下坚实的 TypeScript 基础。

本文将涵盖以下主要内容:

  1. TypeScript 是什么?为什么选择它?
  2. 如何开始使用 TypeScript (安装与编译)。
  3. TypeScript 基础语法:
    • 基本数据类型与类型注解
    • 数组和元组
    • 对象类型、接口 (Interfaces) 与类型别名 (Type Aliases)
    • 函数类型与参数
    • 枚举 (Enums)
    • 联合类型 (Union Types) 与交叉类型 (Intersection Types)
    • 字面量类型
  4. TypeScript 进阶概念初步:
    • 类 (Classes)
    • 泛型 (Generics)
    • 类型断言 (Type Assertion)
    • 类型推断 (Type Inference)
    • 类型守卫 (Type Guards) 与类型窄化 (Type Narrowing)
  5. TypeScript 在实际项目中的应用概览。

准备好了吗?让我们开始 TypeScript 的学习之旅!

第一章:TypeScript 是什么?为什么选择它?

如前所述,TypeScript 是 JavaScript 的超集,它在 JavaScript 的基础上增加了静态类型系统。理解这一点至关重要:

  • 超集 (Superset): 意味着所有 JavaScript 代码都是有效的 TypeScript 代码。你可以逐步地将现有 JavaScript 项目迁移到 TypeScript,而无需一次性重写所有代码。
  • 静态类型 (Static Typing): 区别于 JavaScript 的动态类型。在动态类型语言中,变量的类型是在运行时确定的;而在静态类型语言中,变量的类型在编译时就已确定。TypeScript 的静态类型系统允许你在编写代码时就声明变量、函数参数和返回值的类型,并在编译时检查这些类型是否匹配。
  • 编译 (Compile): TypeScript 代码不能直接在浏览器或 Node.js 中运行。它需要通过 TypeScript 编译器 (tsc) 转换 (编译) 成纯粹的 JavaScript 代码后才能执行。这个编译过程就是类型检查发生的时候。

为什么选择 TypeScript?

选择 TypeScript 的理由有很多,主要集中在其带来的好处:

  1. 提前发现错误 (Early Error Detection): 这是静态类型最显著的优势。类型错误不再需要在程序运行时暴露,而是在你编写代码或编译时就会被 TypeScript 编译器捕获。这大大减少了调试时间,提高了开发效率。
  2. 提高代码质量与可维护性 (Improved Code Quality & Maintainability): 明确的类型定义使得代码意图更加清晰。当你看到一个函数签名 (name: string, age: number): User,你立即就知道这个函数期望接收一个字符串名字和一个数字年龄,并返回一个 User 类型的对象。这就像为代码库添加了实时的、强制执行的文档,降低了理解和修改代码的门槛。
  3. 增强工具支持 (Enhanced Tooling Support): TypeScript 提供了丰富的类型信息,现代 IDE (如 VS Code) 可以利用这些信息提供无与伦比的开发体验:
    • 智能代码补全 (IntelliSense): 输入变量或对象的名称后,IDE 可以准确地提示可用的属性和方法。
    • 代码导航 (Code Navigation): 轻松跳转到变量、函数或类的定义处。
    • 重构 (Refactoring): 安全地重命名变量、提取函数等,IDE 会确保所有引用都得到更新。
    • 实时错误检查 (Live Error Checking): 在你输入代码时,IDE 就会标记出类型错误,无需等待编译。
  4. 更好的团队协作 (Better Team Collaboration): 在多人协作的项目中,清晰的类型定义减少了沟通成本和误解。新成员可以更快地理解代码库结构。接口和类型定义为团队成员之间的代码契约提供了明确的规范。
  5. 支持最新的 JavaScript 特性 (Support for Latest JS Features): TypeScript 总是积极支持最新的 ECMAScript 标准特性,即使这些特性尚未被所有浏览器或 Node.js 版本完全支持。TypeScript 编译器可以将其转换为兼容目标环境的 JavaScript 代码。
  6. 可伸缩性 (Scalability): 对于大型、复杂的应用程序,TypeScript 提供的结构化和类型安全特性使得项目更容易扩展和管理。它能帮助你构建更健壮、更易于理解和修改的大型代码库。

总而言之,虽然 TypeScript 引入了额外的学习成本和编译步骤,但它在项目长期维护、团队协作以及开发效率方面带来的收益,对于中大型项目而言是巨大的。它帮助开发者从容应对 JavaScript 项目复杂性带来的挑战。

第二章:如何开始使用 TypeScript?

开始使用 TypeScript 非常简单,主要包括安装 TypeScript 编译器和了解基本的编译命令。

  1. 安装 TypeScript:
    TypeScript 编译器可以通过 npm (Node Package Manager) 或 yarn 进行安装。通常,我们会选择全局安装,这样 tsc 命令就可以在任何地方使用。

    使用 npm:
    bash
    npm install -g typescript

    使用 yarn:
    bash
    yarn global add typescript

    安装完成后,你可以在终端中运行 tsc --version 来验证安装是否成功并查看 TypeScript 的版本。

    “`bash
    tsc –version

    输出类似 Version 5.x.x

    “`

  2. 编写你的第一个 TypeScript 文件:
    创建一个新文件,命名为 hello.ts(TypeScript 文件的标准扩展名是 .ts)。在文件中输入以下 TypeScript 代码:

    “`typescript
    // hello.ts
    function greet(person: string) {
    return “Hello, ” + person;
    }

    let user = “TypeScript User”;
    // let user = [0, 1, 2]; // 尝试用数组赋值,TypeScript 会报错

    console.log(greet(user));

    // 尝试调用 greet 函数传入错误类型的参数 (这个会在编译时报错)
    // console.log(greet(123)); // Argument of type ‘number’ is not assignable to parameter of type ‘string’.
    ``
    在这个简单的例子中:
    * 我们声明了一个
    greet函数,使用类型注解明确指定了它的参数person必须是string类型。
    * 我们声明了一个变量
    user,并为其赋予了一个字符串值。TypeScript 会根据赋值自动推断出user的类型是string(这就是类型推断,后面会详细介绍)。
    * 我们调用
    greet函数,并传入user变量。因为userstring类型,这符合函数参数的要求,代码是合法的。
    * 被注释掉的代码展示了如果你尝试将一个数组赋值给
    user,或者尝试传入一个数字给greet` 函数,TypeScript 编译器都会发出错误警告。

  3. 编译 TypeScript 文件:
    在终端中,导航到你创建 hello.ts 文件的目录,然后运行 TypeScript 编译器命令 tsc:

    bash
    tsc hello.ts

    如果你的 hello.ts 代码没有类型错误,tsc 命令将会成功执行,并在同一目录下生成一个同名的 JavaScript 文件 hello.js

    javascript
    // hello.js (这是由 tsc hello.ts 生成的 JavaScript 代码)
    function greet(person) {
    return "Hello, " + person;
    }
    var user = "TypeScript User";
    // let user = [0, 1, 2]; // 尝试用数组赋值,TypeScript 会报错
    console.log(greet(user));
    // 尝试调用 greet 函数传入错误类型的参数 (这个会在编译时报错)
    // console.log(greet(123)); // Argument of type 'number' is not assignable to parameter of type 'string'.

    可以看到,生成的 hello.js 代码中,TypeScript 的类型注解 (: string) 已经被移除了,因为它在运行时是不需要的,类型检查的工作已经在编译阶段完成了。

    如果你尝试编译包含类型错误的代码(例如,取消注释 console.log(greet(123));),tsc 命令会输出错误信息,并且默认情况下不会生成 JavaScript 文件。这是 TypeScript 的强大之处——在运行前就告诉你哪里错了。

  4. 运行生成的 JavaScript 文件:
    现在你可以像运行其他 JavaScript 文件一样运行 hello.js

    “`bash
    node hello.js

    输出 Hello, TypeScript User

    “`

  5. 使用 tsconfig.json:
    对于更复杂的项目,你不会希望每次都手动指定要编译的文件。tsconfig.json 文件是 TypeScript 项目的配置文件。它指定了项目的根文件和编译选项。通过使用 tsconfig.json,你可以简单地运行 tsc 命令(不带文件名),TypeScript 编译器就会根据配置文件来编译整个项目。

    在项目根目录创建一个 tsconfig.json 文件:

    json
    {
    "compilerOptions": {
    "target": "es5", // 编译为 ECMAScript 5 版本
    "module": "commonjs", // 使用 CommonJS 模块系统
    "strict": true, // 启用所有严格类型检查选项 (强烈推荐)
    "esModuleInterop": true, // 允许使用 default import 导入 CommonJS/AMD/UMD 模块
    "skipLibCheck": true, // 跳过声明文件 (.d.ts) 的类型检查,加快编译速度
    "forceConsistentCasingInFileNames": true // 强制文件名大小写一致
    // "outDir": "./dist" // 可选:指定编译输出目录
    },
    "include": [
    "src/**/*" // 指定需要编译的文件,这里表示编译 src 文件夹下的所有文件
    ]
    // "exclude": [ // 可选:指定不需要编译的文件或目录
    // "node_modules",
    // "**/*.spec.ts"
    // ]
    }

    这个文件告诉 TypeScript 编译器如何处理你的代码。compilerOptions 中包含各种编译设置,例如 target 指定了编译成的 JavaScript 版本,module 指定了模块系统,strict: true 是一个非常重要的选项,它会启用一系列严格的类型检查规则,帮助你写出更安全的代码。

    创建 tsconfig.json 后,将你的 .ts 文件放在 include 指定的目录(例如 src 目录)下,然后在项目根目录直接运行 tsc 命令即可。

    “`bash

    假设 hello.ts 放在 src 目录下

    项目结构:

    my-ts-project/

    |- tsconfig.json

    |- src/

    |- hello.ts

    在 my-ts-project/ 目录下运行:

    tsc
    “`

    TypeScript 编译器将根据 tsconfig.json 的配置查找并编译 src 目录下的所有 .ts 文件。

至此,你已经学会了如何安装 TypeScript、编写并编译一个简单的 .ts 文件,以及如何使用 tsconfig.json 来配置你的 TypeScript 项目。接下来,我们将深入探讨 TypeScript 的核心——类型系统。

第三章:TypeScript 基础语法

TypeScript 的核心在于其类型系统。通过类型注解,你可以告诉编译器变量、函数、对象等预期的数据类型。

3.1 基本数据类型与类型注解

TypeScript 支持所有 JavaScript 的基本数据类型,并增加了一些自己的类型。

  • boolean: 布尔值,truefalse
    typescript
    let isDone: boolean = false;
  • number: 数字,包括整数和浮点数。
    typescript
    let decimal: number = 6;
    let hex: number = 0xf00d;
    let binary: number = 0b1010;
    let octal: number = 0o744;
  • string: 字符串,可以使用单引号 '、双引号 " 或模板字符串 `
    typescript
    let color: string = "blue";
    color = 'red';
    let fullName: string = `Bob Bobbington`;
    let age: number = 37;
    let sentence: string = `Hello, my name is ${ fullName }. I'll be ${ age + 1 } years old next month.`;
  • nullundefined: 分别代表 nullundefined 值。默认情况下,它们是所有其他类型的子类型,这意味着你可以将 nullundefined 赋值给其他类型的变量(例如 number | null)。然而,当 strictNullChecks 选项启用时(强烈推荐在 tsconfig.json 中设置 strict: true,其中包含了 strictNullChecks),你只能将它们赋值给 nullundefined 类型本身,或者包含 nullundefined 的联合类型。
    “`typescript
    let u: undefined = undefined;
    let n: null = null;

    // 当 strictNullChecks 为 true 时:
    let num: number = 1;
    // num = null; // Error
    // num = undefined; // Error

    let numOrNull: number | null = 1;
    numOrNull = null; // OK
    * **symbol:** 代表唯一的、不可变的原始值。常用于对象属性的键。typescript
    let sym1: symbol = Symbol(‘key1’);
    let sym2: symbol = Symbol(‘key2’);
    // console.log(sym1 === sym2); // false
    * **bigint:** 大整数类型,用于表示任意精度的整数。typescript
    let bigIntValue: bigint = 9007199254740991n;
    “`

  • any: 表示可以是任何类型。当你不知道变量的类型,或者希望跳过类型检查时,可以使用 any应当尽量避免使用 any,因为它失去了 TypeScript 带来的类型安全优势。
    typescript
    let notSure: any = 4;
    notSure = "maybe a string instead";
    notSure = false; // okay, definitely a boolean

    使用 any 就像回到了原生 JavaScript,没有类型检查:
    typescript
    let looseVar: any = { x: 1 };
    looseVar.foo(); // 编译时不会报错,运行时可能会报错

  • unknown: 类似 any,但更安全。unknown 类型的变量在被赋值给其他变量或执行方法之前,必须进行类型检查或类型断言。
    “`typescript
    let possiblyAny: any = “hello”;
    let certainlyUnknown: unknown = “world”;

    let s1: string = possiblyAny; // OK, any 可以赋值给任何类型
    // let s2: string = certainlyUnknown; // Error: Type ‘unknown’ is not assignable to type ‘string’.

    // 需要进行类型检查或断言后才能使用 unknown 变量
    if (typeof certainlyUnknown === ‘string’) {
    let s3: string = certainlyUnknown; // OK
    }
    let s4: string = certainlyUnknown as string; // OK, 使用类型断言
    ``unknown` 是处理不确定类型但又需要类型安全时的首选。

  • void: 通常用于表示函数没有返回值。
    “`typescript
    function warnUser(): void {
    console.log(“This is my warning message”);
    // return “abc”; // Error: Type ‘string’ is not assignable to type ‘void’.
    }

    let unusable: void = undefined;
    // unusable = null; // 当 strictNullChecks 为 true 时,会报错
    “`

  • never: 表示那些永远不会返回值的函数的返回类型,例如抛出异常的函数或包含无限循环的函数。它与 void 的区别在于,void 函数可以正常执行完毕并返回 undefined (虽然不显式返回任何东西),而 never 函数永远不会到达终点。
    “`typescript
    // 返回 never 的函数必须存在无法达到的终点
    function error(message: string): never {
    throw new Error(message);
    }

    // 推断的返回值类型为 never
    function infiniteLoop(): never {
    while (true) {
    }
    }

    // let x: number = error(“Something failed”); // OK, never 可以赋值给任何类型 (尽管没有实际意义)
    // let y: void = error(“Another failure”); // OK
    // let z: string = infiniteLoop(); // OK
    “`

3.2 数组 (Arrays) 和元组 (Tuples)

  • 数组: 表示同类型元素的列表。有两种常见的表示方式:

    • 元素类型后跟 []: type[]
    • 使用泛型数组类型: Array<type>
      “`typescript
      let list: number[] = [1, 2, 3];
      let names: string[] = [“Alice”, “Bob”];
      let genericList: Array = [4, 5, 6];

    // list.push(“hello”); // Error: Argument of type ‘string’ is not assignable to parameter of type ‘number’.
    “`

  • 元组 (Tuple): 表示一个已知元素数量和类型的数组,各元素的类型可以不同。元组的类型顺序必须与元素的顺序严格匹配。
    “`typescript
    // 声明一个包含 string 和 number 的元组
    let x: [string, number];
    // 初始化
    x = [“hello”, 10]; // OK
    // x = [10, “hello”]; // Error: Type ‘number’ is not assignable to type ‘string’ in position 0.

    // 访问元素时,会根据索引提供正确的类型
    console.log(x[0].substr(1)); // OK, x[0] 是 string
    // console.log(x[1].substr(1)); // Error: Property ‘substr’ does not exist on type ‘number’.

    // 元组越界访问 (新版本 TS 会给出警告或错误,取决于配置)
    // x[2] = “world”; // Error: Tuple type ‘[string, number]’ of length 2 has no element at index 2.
    // console.log(x[5].toString()); // Error
    ``
    元组在表示固定结构的数据对时非常有用,例如函数的返回值可能是一个包含状态码和具体结果的数组
    [number, string]`。

3.3 对象类型、接口 (Interfaces) 与类型别名 (Type Aliases)

JavaScript 中对象是核心概念,TypeScript 提供了强大的方式来描述对象的结构。

  • 匿名对象类型: 直接使用 {} 语法描述对象结构。
    “`typescript
    let point: { x: number; y: number } = { x: 10, y: 20 };

    // point.x = “hello”; // Error: Type ‘string’ is not assignable to type ‘number’.
    // point = { x: 10 }; // Error: Property ‘y’ is missing in type ‘{ x: number; }’.
    “`

  • 接口 (Interfaces): 接口是描述对象形状(属性、方法)的一种方式。它们是 TypeScript 中非常重要的概念,尤其适合描述对象、类的结构。
    “`typescript
    interface LabelledValue {
    label: string;
    size: number;
    // 可选属性,使用 ? 标记
    color?: string;
    // 只读属性,使用 readonly 标记
    readonly created: Date;
    // 方法签名
    greet?(message: string): void;
    }

    function printLabel(labelledObj: LabelledValue) {
    console.log(labelledObj.label);
    console.log(labelledObj.size);
    if (labelledObj.color) {
    console.log(labelledObj.color);
    }
    // labelledObj.created = new Date(); // Error: Cannot assign to ‘created’ because it is a read-only property.
    if (labelledObj.greet) {
    labelledObj.greet(“Hello!”);
    }
    }

    let myObj = { size: 10, label: “Size 10 Object”, created: new Date() };
    printLabel(myObj); // OK, myObj 匹配 LabelledValue 接口的形状 (结构子类型检查)

    let myColoredObj = { size: 20, label: “Size 20 Object”, color: “red”, created: new Date(), greet: (msg: string) => console.log(msg) };
    printLabel(myColoredObj); // OK
    ``
    接口的特点:
    * 只存在于 TypeScript 编译阶段,编译后的 JavaScript 代码中不存在接口。
    * 主要用于描述对象的结构。
    * 可以通过
    extends` 关键字继承其他接口。
    * 可以通过多次声明同名接口来合并 (Declaration Merging)。

  • 类型别名 (Type Aliases): 允许你为任何类型创建一个新名称,包括基本类型、联合类型、交叉类型、元组、函数签名等。使用 type 关键字。
    “`typescript
    // 为 string 创建别名
    type MyString = string;
    let s: MyString = “hello”;

    // 为对象类型创建别名 (与接口类似)
    type Point = {
    x: number;
    y: number;
    };

    function printPoint(p: Point) {
    console.log(x: ${p.x}, y: ${p.y});
    }
    printPoint({ x: 100, y: 200 }); // OK

    // 类型别名也可以用于联合类型、交叉类型等
    type ID = number | string; // 联合类型
    type Result = { success: boolean; data: any } | { success: boolean; error: string }; // 联合类型

    let userId: ID = 123;
    userId = “abc”; // OK

    // 类型别名也可以用于函数签名
    type GreetFunction = (name: string) => string;

    const greet: GreetFunction = (n) => Hello, ${n}!;
    console.log(greet(“TypeScript”)); // OK
    “`

  • 接口 (Interface) vs 类型别名 (Type Alias): 它们在很多情况下可以互换使用来描述对象结构。然而,它们之间有一些区别:

    • 扩展/继承: 接口使用 extends 关键字来扩展;类型别名使用交叉类型 & 来组合。
    • 声明合并: 同名接口可以多次声明并会自动合并;同名类型别名则会报错。
    • 表示类型范围: 类型别名可以为任何类型创建别名(包括基本类型、联合/交叉类型等),而接口主要用于描述对象形状。
    • 实现: 类只能实现接口 (implements),不能实现类型别名定义的类型(除非该类型别名是指向一个接口)。

    在实践中,描述对象形状时,使用接口通常更具惯用性(尤其是在定义公共 API 或库时),因为它支持声明合并。但如果你需要表示联合类型、交叉类型、函数类型等复杂类型,或者只需要一个简单的别名,类型别名是更好的选择。

3.4 函数 (Functions)

TypeScript 允许你为函数参数和返回值添加类型注解。

“`typescript
// 带有参数类型和返回值类型的函数
function add(x: number, y: number): number {
return x + y;
}

let mySum = add(5, 3);
// let mySumError = add(5, “3”); // Error: Argument of type ‘string’ is not assignable to parameter of type ‘number’.
// let anotherSum: string = add(5, 3); // Error: Type ‘number’ is not assignable to type ‘string’.

// 函数表达式
let myAdd: (baseValue: number, increment: number) => number =
function(x: number, y: number): number { return x + y; };

// 使用类型别名定义函数类型
type MathOperation = (a: number, b: number) => number;
let multiply: MathOperation = function(a, b) { return a * b; }; // 参数类型和返回值类型可以省略,由类型别名推断

// 可选参数,使用 ? 标记,必须在必选参数之后
function buildName(firstName: string, lastName?: string): string {
if (lastName) {
return firstName + ” ” + lastName;
} else {
return firstName;
}
}

let result1 = buildName(“Bob”); // OK
let result2 = buildName(“Bob”, “Adams”); // OK
// let result3 = buildName(“Bob”, “Adams”, “Sr.”); // Error: Expected 2 arguments, but got 3.

// 默认参数,有默认值的参数可以放在必选参数之后,也可以放在必选参数之前 (不常见)
function buildNameWithDefault(firstName: string, lastName = “Smith”): string {
return firstName + ” ” + lastName;
}
let result4 = buildNameWithDefault(“Bob”); // OK, lastName 会是 “Smith”
let result5 = buildNameWithDefault(“Bob”, “Adams”); // OK, lastName 会是 “Adams”

// 剩余参数 (…rest)
function buildArray(firstElement: number, …restOfElements: number[]): number[] {
return [firstElement, …restOfElements];
}
let arr1 = buildArray(1, 2, 3, 4); // [1, 2, 3, 4]
// let arr2 = buildArray(1, 2, “3”); // Error: Type ‘string’ is not assignable to type ‘number’.

// 函数重载 (Function Overloads): 允许你定义多个具有相同名称但参数类型或数量不同的函数签名。通过重载列表来定义所有可能的调用方式,然后在实现体中使用最通用的参数类型。
function pickCard(x: {suit: string; card: number}[]): number;
function pickCard(x: number): {suit: string; card: number};
function pickCard(x: any): any { // 实现签名,参数类型通常设置为 any 或联合类型以兼容所有重载
// Check to see if we’re working with an object/array
// otherwise assume it’s the index
if (Array.isArray(x)) {
// pick a random card from the deck
let pickedCard = Math.floor(Math.random() * x.length);
return pickedCard;
}
// Otherwise just let them pick the card
else if (typeof x === ‘number’) {
let pickedSuit = Math.floor(x / 13);
return { suit: “spades”, card: x % 13 }; // Simplistic example
} else {
throw new Error(“Invalid argument type”);
}
}

let deck = [{ suit: “diamonds”, card: 2 }, { suit: “spades”, card: 10 }, { suit: “clubs”, card: 13 }];
let pickedCard1 = pickCard(deck); // OK, calls first signature, pickedCard1 is number
console.log(“card: ” + deck[pickedCard1].card + ” of ” + deck[pickedCard1].suit);

let pickedCard2 = pickCard(15); // OK, calls second signature, pickedCard2 is {suit: string; card: number}
console.log(“card: ” + pickedCard2.card + ” of ” + pickedCard2.suit);

// pickCard(“hello”); // Error: No overload matches this call.
“`
函数重载允许在类型层面描述 JavaScript 函数灵活的调用方式。

3.5 枚举 (Enums)

枚举是 TypeScript 新增的数据类型,用于定义一组命名的常量集合。枚举可以提高代码的可读性和可维护性。

  • 数字枚举 (Numeric Enums): 默认情况下,枚举成员的值从 0 开始递增。
    “`typescript
    enum Direction {
    Up, // 0
    Down, // 1
    Left, // 2
    Right // 3
    }

    let go: Direction = Direction.Up;
    console.log(go); // 输出 0

    // 可以手动指定初始值
    enum DirectionWithStart {
    Up = 1,
    Down, // 2
    Left, // 3
    Right // 4
    }

    // 可以手动指定所有值
    enum FileAccess {
    // constant members
    None, // 0
    Read = 1 << 1, // 2
    Write = 1 << 2, // 4
    ReadWrite = Read | Write, // 6
    // computed member
    G = “123”.length // 3
    }
    数字枚举还支持反向映射:可以通过枚举值获取到枚举名称。typescript
    enum Direction {
    Up, Down, Left, Right
    }
    let directionName = Direction[Direction.Up]; // “Up”
    console.log(directionName); // 输出 “Up”
    “`

  • 字符串枚举 (String Enums): 枚举成员的值必须是字符串字面量。字符串枚举不支持反向映射。
    “`typescript
    enum Direction {
    Up = “UP”,
    Down = “DOWN”,
    Left = “LEFT”,
    Right = “RIGHT”,
    }

    let move: Direction = Direction.Up;
    console.log(move); // 输出 “UP”
    // console.log(Direction[“UP”]); // Error: Element implicitly has an ‘any’ type because expression of type ‘”UP”‘ can’t be used to index type ‘typeof Direction’. (字符串枚举不支持反向映射)
    “`
    字符串枚举的好处是,编译后的代码中,引用枚举成员会直接使用字符串字面量,这有助于提高可读性。

  • 异构枚举 (Heterogeneous Enums): 枚举可以混合数字和字符串成员,但不推荐这样做,因为它容易混淆。

3.6 联合类型 (Union Types) 与交叉类型 (Intersection Types)

  • 联合类型 (Union Types): 表示变量可以取多种类型中的任意一种。使用 | 符号连接。
    “`typescript
    // 变量可以是 string 或 number
    let myId: number | string;
    myId = 123; // OK
    myId = “abc”; // OK
    // myId = true; // Error: Type ‘boolean’ is not assignable to type ‘string | number’.

    // 函数参数或返回值可以是联合类型
    function printId(id: number | string): void {
    // 在使用联合类型变量时,需要进行类型检查 (类型守卫/窄化) 来确定具体类型
    if (typeof id === ‘string’) {
    console.log(id.toUpperCase()); // OK, 在这个块内 id 被窄化为 string
    } else {
    console.log(id.toFixed(2)); // OK, 在这个块内 id 被窄化为 number
    }
    }

    printId(100.456); // 输出 100.46
    printId(“hello”); // 输出 HELLO
    “`
    联合类型非常灵活,常用于函数参数、返回值或属性可能具有多种类型的情况。

  • 交叉类型 (Intersection Types): 将多个类型合并为一个新类型。新类型拥有所有被合并类型的所有成员。使用 & 符号连接。
    “`typescript
    interface Person {
    name: string;
    age: number;
    }

    interface Employee {
    company: string;
    salary: number;
    }

    // Student 类型同时拥有 Person 和 Employee 的所有属性
    type Student = Person & Employee;

    let student: Student = {
    name: “Alice”,
    age: 20,
    company: “University XYZ”,
    salary: 0 // 学生可能没有薪水,这里只是示例,实际中可能需要可选属性或更复杂的类型
    };

    console.log(student.name); // OK, 来自 Person
    console.log(student.company); // OK, 来自 Employee
    // console.log(student.address); // Error: Property ‘address’ does not exist on type ‘Student’.
    ``
    交叉类型常用于组合现有类型,创建具有更多特性的新类型。比如,一个对象可能同时具备
    DraggableResizable的特性,可以使用交叉类型Draggable & Resizable` 来描述。

3.7 字面量类型 (Literal Types)

字面量类型允许你将变量的值限定为特定的原始类型字面量(字符串、数字、布尔值)。这使得类型定义更加精确。

“`typescript
// 字符串字面量类型
let direction: “up” | “down” | “left” | “right”;
direction = “up”; // OK
direction = “down”; // OK
// direction = “forward”; // Error: Type ‘”forward”‘ is not assignable to type ‘”up” | “down” | “left” | “right”‘.

// 数字字面量类型
let statusCode: 200 | 404 | 500;
statusCode = 200; // OK
// statusCode = 201; // Error

// 布尔字面量类型 (通常直接使用 boolean)
let isEnabled: true;
isEnabled = true; // OK
// isEnabled = false; // Error

// 字面量类型与联合类型结合使用非常常见
type Alignment = “left” | “right” | “center”;
function setAlignment(align: Alignment): void {
// …
}
setAlignment(“center”); // OK
// setAlignment(“justify”); // Error
“`
字面量类型能够捕获更多的错误,并为代码意图提供更清晰的文档。

第四章:TypeScript 进阶概念初步

本章将简要介绍一些更高级的 TypeScript 概念,这些概念在构建复杂应用时非常有用。

4.1 类 (Classes)

TypeScript 对 ES6 的类提供了更强大的支持,包括类型注解、成员访问修饰符等。

“`typescript
class Greeter {
// 属性,可以带有类型注解和访问修饰符
private name: string; // 私有属性,只能在类的内部访问
readonly greeting: string = “Hello”; // 只读属性,初始化后不能修改
public age: number; // 公有属性 (默认)
protected city: string = “Beijing”; // 受保护属性,只能在类及其子类内部访问

// 构造函数
constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
}

// 方法,可以带有参数和返回类型注解
greet(): string {
    // console.log(this.name); // OK, private 成员在类内部可访问
    // console.log(this.city); // OK, protected 成员在类内部可访问
    return this.greeting + ", " + this.name + "!";
}

}

let greeter = new Greeter(“World”, 30);
console.log(greeter.greet()); // 输出 “Hello, World!”
// console.log(greeter.name); // Error: Property ‘name’ is private and only accessible within class ‘Greeter’.
console.log(greeter.age); // OK, public 成员可访问

// 继承
class Employee extends Greeter {
department: string;

constructor(name: string, age: number, department: string) {
    super(name, age); // 调用父类构造函数
    this.department = department;
}

getElevatorPitch() {
    // console.log(this.name); // Error: name is private
    console.log(this.city); // OK, protected 成员在子类内部可访问
    return `Hello, my name is ${this.greet()} and I work in ${this.department}.`;
}

}

let employee = new Employee(“Alice”, 25, “Sales”);
console.log(employee.getElevatorPitch());

// 实现接口 (Implementing Interfaces): 类可以实现一个或多个接口,强制类满足接口定义的结构。
interface Shape {
color: string;
area(): number;
}

class Circle implements Shape {
color: string;
radius: number;

constructor(color: string, radius: number) {
    this.color = color;
    this.radius = radius;
}

area(): number {
    return Math.PI * this.radius * this.radius;
}

}

let myCircle: Shape = new Circle(“red”, 5);
console.log(myCircle.color); // red
console.log(myCircle.area()); // Pi * 25
“`

4.2 泛型 (Generics)

泛型是 TypeScript 中强大的特性,它允许你编写可以处理多种数据类型的组件,同时保持类型安全。泛型常用于函数、接口和类中。

“`typescript
// 泛型函数
// T 是类型变量,代表函数参数 arr 中的元素类型
function identity(arg: T): T {
return arg;
}

let output1 = identity(“myString”); // 明确指定 T 为 string
let output2 = identity(“myNumber”); // 类型推断:T 被推断为 string

console.log(output1); // myString
console.log(output2); // myNumber

// 泛型数组
function loggingIdentity(arg: T[]): T[] {
console.log(arg.length); // OK, 数组有 length 属性
return arg;
}
// 或使用 Array
function loggingIdentityGeneric(arg: Array): Array {
console.log(arg.length); // OK
return arg;
}

let list1 = loggingIdentity([1, 2, 3]); // T 被推断为 number
let list2 = loggingIdentity([“a”, “b”, “c”]); // 明确指定 T 为 string

// 泛型接口
interface GenericIdentityFn {
(arg: T): T;
}

function identityFn(arg: T): T {
return arg;
}

let myIdentity: GenericIdentityFn = identityFn; // T 在接口使用时被指定为 number
// let myIdentityError: GenericIdentityFn = identityFn; // Error: Types of parameters ‘arg’ are incompatible. (如果 identityFn 不是泛型函数的话)

// 泛型类
class GenericNumber {
zeroValue: T;
add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };

console.log(myGenericNumber.add(5, 10)); // 15

let stringNumeric = new GenericNumber();
stringNumeric.zeroValue = “”;
stringNumeric.add = function(x, y) { return x + y; }; // 字符串连接

console.log(stringNumeric.add(stringNumeric.zeroValue, “test”)); // test
“`
泛型使得代码更灵活,可以在不同类型上重用相同的逻辑,同时编译器会保证类型安全。

4.3 类型断言 (Type Assertion)

有时,你比 TypeScript 编译器更了解某个变量的实际类型。在这种情况下,你可以使用类型断言来告诉编译器,“相信我,我知道这个变量是什么类型”。类型断言不是类型转换,它只是在编译时起作用,不影响运行时代码。

有两种形式:

  • 尖括号语法 (Angle-bracket syntax): <Type>value
    typescript
    let someValue: any = "this is a string";
    let strLength: number = (<string>someValue).length;
    console.log(strLength); // 16
  • as 语法 (Preferred in TSX/React): value as Type
    “`typescript
    let anotherValue: any = “this is also a string”;
    let anotherStrLength: number = (anotherValue as string).length;
    console.log(anotherStrLength); // 16

    // 当处理联合类型时,也常用断言
    function getLength(value: string | number): number {
    // return value.length; // Error: Property ‘length’ does not exist on type ‘string | number’.
    return (value as string).length; // 运行时如果传入数字会出错,但编译器相信了你
    }
    // 更好的做法是使用类型守卫
    ``as语法在 JSX/TSX 文件中更常用,因为尖括号语法` 可能会与 JSX 标签语法冲突。

重要提示: 类型断言绕过了 TypeScript 的类型检查。只在你确定变量的真实类型时使用,否则可能会引入运行时错误。通常,更好的做法是使用类型守卫或类型窄化。

4.4 类型推断 (Type Inference)

TypeScript 并非总需要显式的类型注解。在许多情况下,编译器可以根据变量的赋值或函数的返回值自动推断出其类型。这减轻了编写类型注解的负担,同时仍然保留了类型安全。

“`typescript
// 根据赋值推断类型
let x = 3; // 推断为 number
// x = “hello”; // Error: Type ‘string’ is not assignable to type ‘number’.

let y = [1, 2, null]; // 推断为 (number | null)[]

// 根据函数返回值推断类型
function add(a: number, b: number) { // 参数类型需要注解,或者根据上下文推断
return a + b; // 返回值被推断为 number
}

let result = add(5, 3); // result 被推断为 number

// 根据上下文推断
window.onmousedown = function(mouseEvent) {
console.log(mouseEvent.button); // OK
// console.log(mouseEvent.buton); // Error! TypeScript 知道 mouseEvent 是 MouseEvent 类型
};
“`
类型推断让 TypeScript 代码看起来更简洁,但在关键位置(如函数参数、公共 API 的返回值)仍然建议显式添加类型注解,以提高代码可读性和清晰度。

4.5 类型守卫 (Type Guards) 与类型窄化 (Type Narrowing)

类型窄化是指 TypeScript 编译器在特定的代码块中能够确定变量更精确的类型。实现类型窄化通常通过类型守卫来完成。类型守卫是一种在运行时检查类型的表达式,它能够“收窄”变量在特定范围内的类型。

“`typescript
// 使用 typeof 类型守卫
function printLength(value: string | number): void {
if (typeof value === ‘string’) {
// 在这个块内,value 被窄化为 string 类型
console.log(value.length); // OK
} else {
// 在这个块内,value 被窄化为 number 类型
console.log(value.toString().length); // OK
}
}

// 使用 instanceof 类型守卫
class Dog {
bark() { console.log(“Woof!”); }
}
class Cat {
meow() { console.log(“Meow!”); }
}

function animalSound(animal: Dog | Cat): void {
if (animal instanceof Dog) {
// 在这个块内,animal 被窄化为 Dog 类型
animal.bark(); // OK
} else {
// 在这个块内,animal 被窄化为 Cat 类型
animal.meow(); // OK
}
}

// 使用 in 类型守卫 (检查对象是否包含某个属性)
interface Car {
drive(): void;
}
interface Bike {
pedal(): void;
}

function isCar(vehicle: Car | Bike): vehicle is Car {
return (vehicle as Car).drive !== undefined; // 也可以直接使用 ‘drive’ in vehicle
}

function startVehicle(vehicle: Car | Bike): void {
if (‘drive’ in vehicle) { // 使用 in 守卫
// vehicle 被窄化为 Car
vehicle.drive();
} else {
// vehicle 被窄化为 Bike
vehicle.pedal();
}
}

// 用户自定义类型守卫 (predicate): parameterName is Type
interface Bird {
fly(): void;
layEggs(): void;
}
interface Fish {
swim(): void;
layEggs(): void;
}

function isBird(pet: Bird | Fish): pet is Bird {
return (pet as Bird).fly !== undefined;
// return ‘fly’ in pet; // 也可以这样写
}

function movePet(pet: Bird | Fish) {
if (isBird(pet)) { // 调用自定义类型守卫
pet.fly(); // OK, pet 被窄化为 Bird
} else {
pet.swim(); // OK, pet 被窄化为 Fish
}
}
``
类型守卫和窄化是处理联合类型和编写更精确、安全的代码的重要技术。TypeScript 还能通过控制流分析(如
if/elseswitch`、循环等)自动进行类型窄化。

第五章:TypeScript 在实际项目中的应用概览

学习了 TypeScript 的基础语法,你可能会问,如何在实际项目中使用它呢?

  1. 集成到构建流程: TypeScript 代码不能直接运行,需要编译。常见的做法是将其集成到前端构建工具(如 Webpack, Rollup, Parcel)或 Node.js 项目的构建脚本中。这些工具都有相应的 loader 或 plugin 来处理 .ts 文件。

    • Webpack: 使用 ts-loaderawesome-typescript-loader
    • Rollup: 使用 @rollup/plugin-typescriptrollup-plugin-typescript2
    • Parcel: 内置支持。
    • Node.js 项目: 可以直接使用 tsc 命令编译,或者使用 ts-node (用于开发和运行,无需预编译)。
  2. 框架支持: 主流前端框架(React, Angular, Vue)和后端框架(NestJS, Express 等)都对 TypeScript 提供了非常好的支持。

    • Angular: 整个框架就是用 TypeScript 编写的,项目默认使用 TypeScript。
    • React: 通过 Create React App 或 Vite 创建项目时,可以选择 TypeScript 模板。许多 React 库提供了 TypeScript 定义。
    • Vue: Vue 3 官方完全拥抱 TypeScript,提供 Composition API 和完整的类型推导支持。Vue CLI 或 Vite 创建项目时也可以选择 TypeScript。
  3. 类型声明文件 (.d.ts): JavaScript 生态系统中存在大量的第三方库。为了让 TypeScript 能够理解这些库的结构并提供类型检查和智能提示,社区维护了一个名为 DefinitelyTyped 的仓库,提供了大量流行 JavaScript 库的类型声明文件 (.d.ts 文件)。
    安装这些声明文件通常很简单,例如为 lodash 安装类型声明:
    bash
    npm install --save-dev @types/lodash

    或者使用 yarn:
    bash
    yarn add --dev @types/lodash

    安装后,你就可以在 TypeScript 代码中导入并使用 lodash,同时享受类型提示和检查。

  4. 逐步迁移: 对于现有的 JavaScript 项目,你不需要一次性全部迁移到 TypeScript。你可以从关键模块或新开发的模块开始,逐步引入 .ts 文件,配置 tsconfig.json,让 TypeScript 和 JavaScript 文件共存,逐步享受 TypeScript 带来的好处。

结论

TypeScript 凭借其强大的静态类型系统、优秀的工具支持和与 JavaScript 生态系统的无缝集成,已经成为现代 JavaScript 开发中越来越重要的工具。它能帮助开发者编写更健壮、可维护、易于理解的代码,特别是在构建大型应用和进行团队协作时,其优势更加明显。

本文介绍了 TypeScript 的核心概念,包括基本类型、接口、类型别名、函数、枚举、泛型、类型断言、类型推断以及类型守卫等。这些是掌握 TypeScript 的基础。

当然,TypeScript 的世界远不止于此。还有许多高级特性等待你去探索,例如装饰器 (Decorators)、模块 (Modules)、命名空间 (Namespaces)、条件类型 (Conditional Types)、映射类型 (Mapped Types) 等。

学习 TypeScript 是一个渐进的过程。从今天开始,尝试在你的新项目中使用 TypeScript,或者在你现有的 JavaScript 项目中引入它。通过实践,你将逐渐体会到 TypeScript 带来的便利和价值。

希望这篇详细的中文基础介绍能帮助你快速踏入 TypeScript 的大门!祝你编码愉快!


发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部