TypeScript 指南 – wiki基地


深入探索 TypeScript:一份全面的开发者指南

随着现代 Web 应用变得日益复杂,JavaScript 的动态特性在带来灵活性的同时,也引入了潜在的运行时错误和维护挑战。尤其是在大型项目或团队协作中,代码的可读性、可维护性和健壮性变得尤为重要。正是在这样的背景下,TypeScript 应运而生,并迅速成为前端和后端开发领域不可或缺的工具。

TypeScript 是 JavaScript 的一个超集,它在 JavaScript 的基础上添加了静态类型系统和面向对象特性(如类和接口)。它最终会被编译成纯粹的 JavaScript 代码,因此可以在任何支持 JavaScript 的环境中运行。本指南将带你深入探索 TypeScript 的世界,从基本概念到高级特性,帮助你掌握这门强大的语言,提升你的开发效率和代码质量。

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

在深入学习 TypeScript 的具体语法和特性之前,我们首先需要理解它为什么如此受欢迎,以及它解决了 JavaScript 的哪些痛点。

1. 静态类型系统:提前捕获错误

这是 TypeScript 最核心的特性。在 JavaScript 中,变量的类型是在运行时确定的,这意味着很多类型相关的错误(比如将字符串和数字相加导致意料之外的结果,或者调用不存在的方法)只有在代码执行时才会暴露。

TypeScript 引入了静态类型系统,允许你在代码编写阶段就声明变量、函数参数、函数返回值等的类型。TypeScript 编译器会在代码编译时检查这些类型声明,并在发现类型不匹配或其他类型错误时立即报错。这大大减少了运行时错误的发生概率,尤其是在重构代码或修改他人代码时,类型检查能提供强大的保障。

“`typescript
// JavaScript
let data = “hello”;
data = 123; // 这是合法的 JavaScript

// TypeScript
let data: string = “hello”;
// data = 123; // 这会引起 TypeScript 编译错误:Type ‘number’ is not assignable to type ‘string’.
“`

2. 增强的开发者体验 (Developer Experience, DX)

静态类型信息为开发者工具(如 IDE、编辑器)提供了强大的支持。

  • 代码补全 (Autocompletion): 当你输入变量或对象名时,编辑器可以根据其类型准确地预测你可能需要访问的属性或方法,提供智能的代码补全建议。
  • 即时错误提示: 在你编写代码时,编辑器就能实时高亮潜在的类型错误,无需运行代码或等待编译结果。
  • 代码导航与重构: 强大的类型信息使得在大型代码库中跳转到定义、查找所有引用、安全地重命名变量或函数等重构操作变得更加容易和可靠。
  • 代码可读性: 类型声明本身就是一种文档,它清晰地表明了代码预期的数据结构和行为,使得代码更易于理解。

3. 提升项目可维护性和可扩展性

随着项目规模的增大,代码量急剧增加,参与开发的成员也可能增多。静态类型系统在大型项目中优势尤为明显:

  • 清晰的接口定义: 通过接口(Interface)或类型别名(Type Alias),可以明确定义模块或组件之间的契约,降低模块间的耦合度,方便团队协作。
  • 更安全的重构: 修改代码时,类型系统会帮助你检查这些修改对代码库其他部分的影响,避免不小心引入破坏性变更。
  • 易于新人上手: 新成员加入项目时,类型信息可以帮助他们更快地理解代码的结构和预期用途。

4. 与 JavaScript 生态系统的无缝集成

TypeScript 是 JavaScript 的超集,这意味着所有合法的 JavaScript 代码本身就是合法的 TypeScript 代码。你可以逐步地将 TypeScript 应用到现有的 JavaScript 项目中,无需一次性重写整个项目。同时,TypeScript 可以方便地使用大量的现有 JavaScript 库和框架,通常通过类型定义文件(.d.ts 文件)来为这些库提供类型信息。

5. 面向对象特性的支持

TypeScript 支持类(Classes)、接口(Interfaces)、模块(Modules)等面向对象特性,这使得使用 TypeScript 可以编写结构化、可维护性更强的代码,尤其适合构建大型的、复杂的应用程序。

第二章:开始使用 TypeScript

现在,让我们学习如何开始使用 TypeScript。

1. 安装 TypeScript

TypeScript 是一个 Node.js 包,你可以通过 npm 或 yarn 进行安装。通常我们会全局安装,方便在任何地方使用 tsc 命令:

“`bash
npm install -g typescript

或者使用 yarn

yarn global add typescript

“`

安装完成后,你可以在命令行中检查 TypeScript 版本:

bash
tsc --version

2. 编译你的第一个 TypeScript 文件

创建一个名为 hello.ts 的文件:

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

let user = “TypeScript User”;
console.log(greet(user));

// 尝试传入错误类型,会报错
// let num = 123;
// console.log(greet(num)); // Type ‘number’ is not assignable to type ‘string’.
“`

在命令行中,使用 tsc 命令编译这个文件:

bash
tsc hello.ts

这会在同一目录下生成一个 hello.js 文件:

javascript
// hello.js
function greet(person) {
return "Hello, " + person;
}
var user = "TypeScript User";
console.log(greet(user));

你可以通过 Node.js 运行生成的 JavaScript 文件:

bash
node hello.js

输出:Hello, TypeScript User

3. 配置项目:tsconfig.json

在实际项目中,你不会只编译一个文件。TypeScript 项目通常使用一个 tsconfig.json 文件来配置编译选项。在一个项目的根目录中运行以下命令可以生成一个默认的 tsconfig.json 文件:

bash
tsc --init

生成的 tsconfig.json 文件包含大量的选项,其中一些关键选项包括:

  • target: 指定编译后的 JavaScript 版本 (e.g., “ES5”, “ES2018”, “ESNext”)。选择合适的版本以兼容你的目标运行环境。
  • module: 指定生成的模块代码规范 (e.g., “CommonJS” for Node.js, “ESNext” for modern browsers or bundlers).
  • strict: 启用一系列严格的类型检查选项。强烈建议将此选项设置为 true,因为它能捕获更多潜在问题,提供更强的类型安全性。
  • outDir: 指定编译生成的 JavaScript 文件存放的目录。
  • rootDir: 指定项目源文件的根目录。
  • include: 一个文件路径数组,指定哪些文件或文件夹应该被包含在编译过程中。
  • exclude: 一个文件路径数组,指定哪些文件或文件夹应该从编译过程中排除。

例如,一个简单的 tsconfig.json 可能看起来像这样:

json
{
"compilerOptions": {
"target": "ES2018",
"module": "CommonJS",
"strict": true,
"outDir": "./dist", // 编译结果放在 dist 目录下
"rootDir": "./src", // 源文件在 src 目录下
"esModuleInterop": true, // 允许使用 CommonJS/AMD/等模块方式导入 ES Module
"forceConsistentCasingInFileNames": true // 强制文件名大小写一致
},
"include": [
"src/**/*.ts" // 包含 src 目录下所有 ts 文件(包括子目录)
],
"exclude": [
"node_modules" // 排除 node_modules 目录
]
}

有了 tsconfig.json 文件后,在项目根目录直接运行 tsc 命令,TypeScript 编译器会查找并使用该文件进行编译。

第三章:TypeScript 核心概念:基础类型

TypeScript 在 JavaScript 的基础上添加了多种类型注解。

1. JavaScript 已有的原始类型

  • number: 任意数字类型,包括整数和浮点数。
    typescript
    let age: number = 30;
    let price: number = 99.99;
  • string: 任意字符串类型。
    typescript
    let name: string = "Alice";
    let message: string = `Hello, ${name}!`;
  • boolean: 布尔值,truefalse
    typescript
    let isDone: boolean = false;
    let hasStarted: boolean = true;
  • null: 表示缺少值,常用于表示变量未指向任何对象。
  • undefined: 表示变量已声明但未赋值。
    typescript
    let nullableValue: string | null = null; // 使用联合类型表示可能为 null
    let undefinedValue: number | undefined = undefined; // 使用联合类型表示可能为 undefined

    在严格模式下 (strictNullChecks: true),nullundefined 只能赋值给它们各自的类型或 any/unknown 或联合类型中包含 null/undefined 的情况。
  • symbol: 表示独一无二的值。
    typescript
    const sym1 = Symbol("id");
    const sym2 = Symbol("id");
    // sym1 === sym2; // false
  • bigint: 表示任意精度的整数。
    typescript
    let largeNumber: bigint = 9007199254740991n;

2. TypeScript 特有的类型

  • any: 表示可以是任何类型。使用 any 会禁用特定变量的类型检查。虽然提供了灵活性,但也失去了 TypeScript 的主要优势,应尽量避免使用,除非你明确知道自己在做什么或者需要处理来自外部、类型未知的数据。
    typescript
    let looselyTyped: any = 4;
    looselyTyped = "a string";
    looselyTyped = false;
    looselyTyped.toFixed(2); // 编译时不会报错,运行时可能出错
  • unknown: 比 any 更安全的类型。表示未知类型。与 any 不同,unknown 类型的变量不能直接进行大多数操作(如调用方法、访问属性),除非你先进行类型检查或类型断言,将其缩小到更具体的类型。
    “`typescript
    let potentiallyAny: unknown = 4;
    // potentiallyAny.toFixed(2); // 编译时报错:Object is of type ‘unknown’.

    if (typeof potentiallyAny === ‘number’) {
    console.log(potentiallyAny.toFixed(2)); // OK,在条件块内 potentiallyAny 被缩小为 number
    }
    * `void`: 通常用于表示函数没有返回值。typescript
    function warnUser(): void {
    console.log(“This is a warning message”);
    }
    * `never`: 表示那些永远不会返回(例如,抛出错误或包含无限循环)的函数的返回值类型,或者那些不可能发生的类型。typescript
    function error(message: string): never {
    throw new Error(message);
    }

    function infiniteLoop(): never {
    while (true) {
    // …
    }
    }
    “`

3. 数组 (Arrays)

表示同类型元素的列表。
typescript
let list: number[] = [1, 2, 3];
let colors: string[] = ["red", "green", "blue"];
let genericList: Array<number> = [1, 2, 3]; // 使用泛型语法

4. 元组 (Tuples)

表示一个已知元素数量和类型的数组,各元素的类型可以不同。
typescript
// 定义一个元组,第一个元素是字符串,第二个是数字
let x: [string, number];
x = ["hello", 10]; // OK
// x = [10, "hello"]; // Error: Type 'number' is not assignable to type 'string' at index 0.
// x[0] = 99; // Error: Type 'number' is not assignable to type 'string'.
console.log(x[0].substring(1)); // OK
// console.log(x[1].substring(1)); // Error: Property 'substring' does not exist on type 'number'.

5. 枚举 (Enums)

为一组数值赋予友好的名字。默认情况下,枚举从 0 开始编号。你也可以手动设置成员的值。
“`typescript
enum Color {Red, Green, Blue}
let c: Color = Color.Green;
console.log(c); // 输出 1

enum Status {Success = 200, NotFound = 404, Error = 500}
let status: Status = Status.Success;
console.log(status); // 输出 200
console.log(Status[404]); // 输出 “NotFound” (反向映射)

// 字符串枚举
enum Direction {
Up = “UP”,
Down = “DOWN”,
Left = “LEFT”,
Right = “RIGHT”,
}
let dir: Direction = Direction.Up;
console.log(dir); // 输出 “UP”
// 字符串枚举没有反向映射
“`

第四章:TypeScript 核心概念:对象类型、接口与类型别名

JavaScript 中对象是核心,TypeScript 提供了强大的方式来描述对象的形状。

1. 对象类型 (Object Types)

你可以直接使用内联方式描述对象的结构:
“`typescript
let person: { name: string; age: number; };

person = { name: “Bob”, age: 25 }; // OK
// person = { name: “Charlie” }; // Error: Property ‘age’ is missing.
// person = { name: “David”, age: 30, city: “NY” }; // Error: Object literal may only specify known properties.
“`

2. 接口 (Interfaces)

接口是描述对象形状(包括属性和方法)的一种强大方式。它们定义了一个契约,任何实现这个接口的对象都必须遵循这个契约。

“`typescript
interface User {
name: string;
age: number;
readonly id: number; // 只读属性
email?: string; // 可选属性
}

function greetUser(user: User) {
console.log(Hello, ${user.name}! You are ${user.age} years old.);
if (user.email) {
console.log(Your email is ${user.email}.);
}
// user.id = 101; // Error: Cannot assign to ‘id’ because it is a read-only property.
}

let newUser: User = { id: 100, name: “Alice”, age: 30 };
greetUser(newUser);

let anotherUser: User = { id: 101, name: “Bob”, age: 25, email: “[email protected]” };
greetUser(anotherUser);
“`

接口还可以描述函数类型:
“`typescript
interface SearchFunc {
(source: string, subString: string): boolean;
}

let mySearch: SearchFunc;
mySearch = function(src: string, sub: string): boolean {
const result = src.search(sub);
return result > -1;
}
// mySearch = function(src: number, sub: number): boolean { // Error: 参数类型不匹配
// // …
// }
“`

接口的继承:
“`typescript
interface Shape {
color: string;
}

interface Square extends Shape {
sideLength: number;
}

let square: Square = { color: “blue”, sideLength: 10 };
“`

3. 类型别名 (Type Aliases)

类型别名是给类型起一个新名字。它们可以用于原始类型、联合类型、交叉类型、元组,以及任何其他你可以定义的类型。

“`typescript
type MyString = string; // 给原始类型起别名

type StringOrNumber = string | number; // 联合类型别名

type Point = { // 对象类型别名
x: number;
y: number;
};

let coordinate: Point = { x: 10, y: 20 };

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

let myGreet: GreetFunction = (name) => Hello, ${name};

// 类型别名可以引用自身(递归类型别名,常用于描述链表等结构)
type LinkedList = T & { next: LinkedList | null };
“`

接口与类型别名的区别与选择:

  • 扩展性 (Extensibility): 接口可以被多次声明并会自动合并(Declaration Merging),这在声明第三方库的类型时非常有用。接口也可以使用 extends 关键字来继承其他接口或类。类型别名不能被重复声明合并,也不能被继承,但可以使用交叉类型 (&) 来组合其他类型。
  • 适用范围: 类型别名可以为任何类型创建别名,包括原始类型、联合类型、元组等。接口主要用于描述对象的形状或类的契约。
  • 推荐: 通常情况下,当你需要描述对象或类的形状时,优先使用接口,因为它提供了更好的可扩展性和声明合并特性。当你需要为联合类型、交叉类型、原始类型或其他非对象类型起别名时,或者需要使用一些高级类型特性(如映射类型)时,使用类型别名。

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

  • 联合类型 (|): 表示变量可以是几种类型之一。
    “`typescript
    function printId(id: number | string) {
    console.log(“Your ID is: ” + id);
    }
    printId(101); // OK
    printId(“202”); // OK
    // printId({ myId: 22342 }); // Error

    // 在联合类型中访问成员时,只能访问所有类型共有的成员,或者使用类型缩小
    function printIdSafe(id: number | string) {
    if (typeof id === “string”) {
    console.log(id.toUpperCase()); // OK, id is narrowed to string
    } else {
    console.log(id.toFixed(2)); // OK, id is narrowed to number
    }
    }
    * **交叉类型 (`&`):** 将多个类型合并为一个类型,新类型拥有所有被合并类型的成员。typescript
    interface Person {
    name: string;
    }

    interface Age {
    age: number;
    }

    type PersonWithAge = Person & Age; // 同时拥有 name 和 age 属性

    let someone: PersonWithAge = { name: “Alice”, age: 30 }; // OK
    // let another: PersonWithAge = { name: “Bob” }; // Error: Property ‘age’ is missing.
    “`

5. 字面量类型 (Literal Types)

字面量类型允许你指定变量只能是某些特定的字符串、数字或布尔值。常与联合类型一起使用。

“`typescript
let direction: “up” | “down” | “left” | “right”;
direction = “up”; // OK
// direction = “forward”; // Error: Type ‘”forward”‘ is not assignable to type ‘”up” | “down” | “left” | “right”‘.

let statusCode: 200 | 404 | 500;
statusCode = 200; // OK
// statusCode = 403; // Error
“`

第五章:TypeScript 核心概念:函数与类

1. 函数

TypeScript 对函数提供了强大的类型支持,包括参数类型、返回值类型、可选参数、默认参数、剩余参数和函数重载。

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

// 函数表达式
let myAdd: (x: number, y: number) => number = function(x: number, y: number): number {
return x + y;
};
// 或者利用上下文类型推断简化
let myAddSimplified = function(x: number, y: number): number {
return x + y;
};

// 可选参数 (使用 ?)
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”, “Sr.”); // Error: Expected 1-2 arguments, but got 3.
let result3 = buildName(“Bob”, “Adams”); // OK

// 默认参数 (带默认值的参数会自动被认为是可选的)
function buildNameWithDefault(firstName: string, lastName: string = “Smith”): string {
return firstName + ” ” + lastName;
}
let result4 = buildNameWithDefault(“Bob”); // OK, lastName is “Smith”
let result5 = buildNameWithDefault(“Bob”, “Adams”); // OK, lastName is “Adams”

// 剩余参数 (…)
function sum(first: number, …restOfNumbers: number[]): number {
let total = first;
for (const num of restOfNumbers) {
total += num;
}
return total;
}
let result6 = sum(1, 2, 3, 4, 5); // OK

// 函数重载 (提供多个函数签名)
function heavyOperation(x: number): number;
function heavyOperation(x: string): string;
function heavyOperation(x: number | string): number | string {
// 实现签名必须兼容所有重载签名
if (typeof x === “number”) {
return x * 2;
} else {
return x.toUpperCase();
}
}
let resNum = heavyOperation(10); // resNum 是 number 类型
let resStr = heavyOperation(“hello”); // resStr 是 string 类型
// heavyOperation(true); // Error: 没有匹配的重载签名
“`

2. 类 (Classes)

TypeScript 支持基于类的面向对象编程模式,包括类的定义、构造函数、属性、方法、访问修饰符、继承和实现接口。

“`typescript
class Animal {
// 属性 (可以有访问修饰符)
// public: 默认,类内外都可访问
// private: 只能在类内部访问
// protected: 只能在类内部及其子类中访问
private name: string;
protected species: string;

// 只读属性
readonly origin: string = "Earth";

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

// 方法
public move(distanceInMeters: number = 0) {
    console.log(`${this.name} moved ${distanceInMeters}m.`);
}

// Private 方法
private sayName() {
    console.log(`My name is ${this.name}`);
}

// Protected 方法
protected saySpecies() {
    console.log(`I am a ${this.species}`);
}

}

let cat = new Animal(“Kitty”, “Feline”);
cat.move(10);
// cat.name; // Error: Property ‘name’ is private.
// cat.sayName(); // Error: Property ‘sayName’ is private.

// 继承
class Snake extends Animal {
constructor(name: string) {
super(name, “Reptile”); // 调用父类构造函数
}

public move(distanceInMeters = 5) {
    console.log("Slithering...");
    super.move(distanceInMeters); // 调用父类的 move 方法
}

public identify() {
    // console.log(`I am a ${this.name} ${this.species}`); // Error: 'name' is private in base class 'Animal'.
    console.log(`I am a ${this.species}.`); // OK: 'species' is protected.
    this.saySpecies(); // OK: 'saySpecies' is protected.
}

}

let sam = new Snake(“Sammy”);
sam.move();
sam.identify();

// 实现接口 (Classes implementing interfaces)
interface Disposable {
dispose(): void;
}

interface Gettable {
get(key: string): T;
}

class Resource implements Disposable, Gettable {
private data: { [key: string]: any } = {};

dispose() {
    console.log("Resource disposed.");
    // Clean up logic
}

get<T>(key: string): T {
    return this.data[key] as T; // 可能需要类型断言
}

set<T>(key: string, value: T) {
    this.data[key] = value;
}

}

let res = new Resource();
res.set(“config”, { endpoint: “/api” });
let config = res.get<{ endpoint: string }>(“config”);
console.log(config.endpoint);
res.dispose();
“`

第六章:泛型 (Generics)

泛型是在定义函数、接口或类时,不预先指定具体的类型,而是在使用时再指定类型的一种特性。这使得代码更加灵活、可重用,同时又能保持类型安全性。

1. 为什么使用泛型?

考虑一个简单的函数,它返回你传入的任何参数:

typescript
function identity(arg: any): any {
return arg;
}

这个函数使用了 any 类型,虽然灵活,但丢失了类型信息。调用它时,我们不知道返回值的类型:

typescript
let output = identity("myString"); // output 的类型是 any
console.log(output.length); // 编译时不会报错,但如果传入数字就会运行时报错

如果我们想让函数能够返回与传入参数 相同 的类型,同时又保留类型信息,就可以使用泛型。

2. 泛型函数

“`typescript
function identity(arg: T): T {
return arg;
}

// 使用泛型函数
let output1 = identity(“myString”); // 明确指定 T 为 string,output1 的类型是 string
console.log(output1.length); // OK

let output2 = identity(123); // 利用类型推断,T 被推断为 number,output2 的类型是 number
console.log(output2.toFixed(2)); // OK

// output2.length; // Error: Property ‘length’ does not exist on type ‘number’.
``
这里的
是类型变量,它在函数被调用时才确定具体的类型。常见的类型变量名有T(Type)、K(Key)、V(Value)、E` (Element) 等。

3. 泛型接口

“`typescript
interface GenericIdentityFn {
(arg: T): T;
}

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

let myIdentity: GenericIdentityFn = identity; // myIdentity 的类型是接收 number 参数,返回 number 的函数
console.log(myIdentity(100)); // OK
// console.log(myIdentity(“hello”)); // Error: Argument of type ‘”hello”‘ is not assignable to parameter of type ‘number’.
“`

4. 泛型类

“`typescript
class GenericNumber {
zeroValue: T;
add: (x: T, y: T) => T;

constructor(zeroValue: T, addFunction: (x: T, y: T) => T) {
    this.zeroValue = zeroValue;
    this.add = addFunction;
}

}

let myGenericNumber = new GenericNumber(0, (x, y) => x + y);
console.log(myGenericNumber.add(5, 10)); // 输出 15

let myGenericString = new GenericNumber(“”, (x, y) => x + y);
console.log(myGenericString.add(“hello”, “typescript”)); // 输出 “hellotypescript”
“`

5. 泛型约束 (Generic Constraints)

有时我们希望泛型类型变量具有某些特定的属性或方法。这时可以使用泛型约束。

“`typescript
interface Lengthwise {
length: number;
}

// 要求泛型 T 必须至少拥有一个 number 类型的 length 属性
function loggingIdentity(arg: T): T {
console.log(arg.length); // OK, we know T has a .length property
return arg;
}

// loggingIdentity(3); // Error: Argument of type ‘number’ is not assignable to parameter of type ‘Lengthwise’.
loggingIdentity({ length: 10, value: 3 }); // OK
loggingIdentity(“hello”); // OK, string has a length property
loggingIdentity([1, 2, 3]); // OK, array has a length property
“`

6. keyof 类型操作符

keyof 操作符接受一个对象类型,生成其属性名的联合类型(字符串字面量)。常与泛型约束结合使用。

“`typescript
function getProperty(obj: T, key: K): T[K] {
return obj[key];
}

let obj = { a: 1, b: “hello”, c: true };

let valueA = getProperty(obj, “a”); // valueA is number
let valueB = getProperty(obj, “b”); // valueB is string
// let valueD = getProperty(obj, “d”); // Error: Argument of type ‘”d”‘ is not assignable to parameter of type ‘”a” | “b” | “c”‘.
``
这里的
K extends keyof T约束了类型变量K必须是类型T的属性名之一。T[K]则是索引访问类型,表示获取T类型中属性名是K` 的属性的类型。

第七章:类型断言与类型缩小 (Type Assertions & Type Narrowing)

1. 类型断言 (Type Assertions)

当你比 TypeScript 更清楚某个变量的具体类型时,可以使用类型断言来覆盖类型推断。它不会改变运行时的数据,只在编译时起作用。

有两种形式:尖括号语法 (<Type>) 和 as 语法。在 JSX 中只能使用 as 语法。

“`typescript
let someValue: any = “this is a string”;

// 尖括号语法
let strLength: number = (someValue).length;

// as 语法 (推荐使用)
let strLength2: number = (someValue as string).length;

console.log(strLength); // 输出 16
console.log(strLength2); // 输出 16

let someOtherValue: unknown = “this is another string”;
// 直接访问 length 会报错,因为 unknown 类型未知
// console.log(someOtherValue.length); // Error

// 使用类型断言
let anotherStrLength: number = (someOtherValue as string).length; // OK
console.log(anotherStrLength); // 输出 20
“`

注意: 类型断言是一把双刃剑。如果断言的类型与实际运行时类型不符,可能会导致运行时错误,而 TypeScript 编译器无法捕获。因此,应谨慎使用类型断言。

2. 类型缩小 (Type Narrowing)

类型缩小是 TypeScript 在控制流分析中判断变量类型范围的过程。通过各种检查(如条件语句),可以将一个联合类型或 unknown 类型缩小到更具体的类型。

常见的类型缩小方法:

  • typeof 检查: 用于原始类型(string, number, boolean, symbol, undefined, bigint)。
    typescript
    function print(value: string | number) {
    if (typeof value === 'string') {
    console.log(value.toUpperCase()); // value is string here
    } else {
    console.log(value.toFixed(2)); // value is number here
    }
    }
  • instanceof 检查: 用于类实例。
    “`typescript
    class Dog {
    bark() { console.log(‘Woof!’); }
    }
    class Cat {
    meow() { console.log(‘Meow!’); }
    }

    function makeSound(animal: Dog | Cat) {
    if (animal instanceof Dog) {
    animal.bark(); // animal is Dog here
    } else {
    animal.meow(); // animal is Cat here
    }
    }
    * **`in` 操作符检查:** 用于检查对象是否拥有某个属性。typescript
    interface Car {
    drive: () => void;
    model: string;
    }
    interface Bike {
    ride: () => void;
    brand: string;
    }

    function startVehicle(vehicle: Car | Bike) {
    if (‘drive’ in vehicle) {
    vehicle.drive(); // vehicle is Car here
    } else {
    vehicle.ride(); // vehicle is Bike here
    }
    }
    * **相等性检查 (`==`, `!=`, `===`, `!==`):** 与字面量类型、`null` 或 `undefined` 进行比较。typescript
    function processData(data: string | string[] | null) {
    if (data === null) {
    console.log(“No data.”); // data is null here
    return;
    }

    if (typeof data === 'string') {
        console.log(data.trim()); // data is string here
    } else {
        console.log(data.join(', ')); // data is string[] here
    }
    

    }
    * **真值缩小 (Truthiness Narrowing):** 基于变量在布尔上下文中的真假性。typescript
    function printNonNull(value: string | null | undefined) {
    if (value) {
    console.log(value.length); // value is string here (not null or undefined)
    } else {
    console.log(“Value is null, undefined, or empty string.”); // value is null, undefined, or “” here
    }
    }
    * **判别式联合 (Discriminated Unions):** 一种强大的模式,结合联合类型和字面量属性。typescript
    interface Square {
    kind: “square”; // 判别属性
    sideLength: number;
    }
    interface Circle {
    kind: “circle”; // 判别属性
    radius: number;
    }
    interface Triangle {
    kind: “triangle”; // 判别属性
    sideA: number;
    sideB: number;
    sideC: number;
    }

    type Shape = Square | Circle | Triangle;

    function getArea(shape: Shape): number {
    switch (shape.kind) {
    case “square”:
    return shape.sideLength * shape.sideLength; // shape is Square here
    case “circle”:
    return Math.PI * shape.radius ** 2; // shape is Circle here
    case “triangle”:
    // 使用海伦公式计算面积
    const s = (shape.sideA + shape.sideB + shape.sideC) / 2;
    return Math.sqrt(s * (s – shape.sideA) * (s – shape.sideB) * (s – shape.sideC)); // shape is Triangle here
    default:
    // 使用 never 类型确保处理了所有情况
    const _exhaustiveCheck: never = shape;
    return _exhaustiveCheck;
    }
    }
    “`

第八章:模块与类型声明文件 (.d.ts)

1. 模块 (Modules)

TypeScript 遵循 ES Module 规范,使用 importexport 关键字来组织代码。这有助于避免全局命名空间污染,并清晰地管理代码依赖。

创建一个文件 math.ts:
“`typescript
// math.ts
export function add(x: number, y: number): number {
return x + y;
}

export const PI = 3.14159;

export class Calculator {
add(x: number, y: number): number {
return x + y;
}
}
“`

在另一个文件 app.ts 中使用:
“`typescript
// app.ts
import { add, PI, Calculator } from ‘./math’; // 导入指定成员
// 或者 import * as MathUtils from ‘./math’; // 导入所有成员到命名空间

console.log(add(5, 3)); // 输出 8
console.log(PI); // 输出 3.14159

const calc = new Calculator();
console.log(calc.add(10, 20)); // 输出 30
“`

编译时确保 tsconfig.json 中的 module 选项设置正确 (如 CommonJSESNext)。

2. 类型声明文件 (.d.ts)

JavaScript 世界中有海量的库,它们本身并不是用 TypeScript 编写的,没有内置类型信息。为了让 TypeScript 项目能够安全地使用这些库并获得类型检查和代码补全,社区创建了类型声明文件 (.d.ts 文件)。

  • Purpose: .d.ts 文件只包含类型定义(接口、类型别名、函数签名、变量类型等),不包含实际的实现代码。它们是 TypeScript 与现有 JavaScript 代码沟通的桥梁。
  • 使用 @types/ 库: 大多数流行的 JavaScript 库的类型定义都发布在 npm 的 @types/ 组织下。你可以通过 npm 安装它们:
    bash
    npm install @types/lodash @types/react --save-dev
    # 或者 yarn add @types/lodash @types/react --dev

    安装后,TypeScript 编译器会自动找到这些声明文件,并为你使用的库提供类型信息。例如,安装 @types/lodash 后,你就可以在 .ts 文件中导入并使用 lodash,并获得完整的类型检查和代码补全。

    “`typescript
    import _ from ‘lodash’;

    const numbers = [1, 2, 3, 4, 5];
    const doubled = _.map(numbers, n => n * 2); // TypeScript knows map function signature
    console.log(doubled); // Output: [2, 4, 6, 8, 10]

    // _.sortBy(“hello”, “world”); // Error: Argument of type ‘string’ is not assignable to parameter of type ‘Many‘.
    ``
    * **编写自己的
    .d.ts文件:** 如果你要使用一个没有@types库的 JavaScript 库,或者需要为自己的 JavaScript 代码库提供类型定义,你可以手动编写.d.ts` 文件。这通常涉及到声明变量、函数、类、模块的类型。

    typescript
    // my-js-lib.js
    exports.greet = function(name) {
    return "Hello, " + name;
    };
    exports.version = "1.0.0";

    typescript
    // my-js-lib.d.ts
    declare module 'my-js-lib' { // 声明一个模块
    export function greet(name: string): string; // 声明导出函数及其类型
    export const version: string; // 声明导出变量及其类型
    }

    然后在 .ts 文件中就可以像使用 TypeScript 模块一样使用它:
    “`typescript
    import { greet, version } from ‘my-js-lib’;

    console.log(greet(“TypeScript”)); // OK
    console.log(version); // OK
    // console.log(greet(123)); // Error
    “`

第九章:TypeScript 的集成与工具

TypeScript 与现代开发工具和框架有着出色的集成。

  • IDE/编辑器支持: Visual Studio Code (VS Code) 是 TypeScript 的官方推荐编辑器,提供了无与伦比的支持,包括智能代码补全、错误提示、重构工具、代码导航等。其他编辑器(如 WebStorm, Atom, Sublime Text)也通过插件提供了不错的 TypeScript 支持。
  • 构建工具:
    • Webpack, Rollup, Parcel: 现代模块打包工具通常都有相应的 TypeScript loader (如 ts-loader@babel/preset-typescript),可以在打包过程中直接处理 .ts 文件。
    • TSC: TypeScript 编译器 (tsc) 本身也可以作为构建工具,用于编译项目。
  • 框架集成:
    • Angular: 从一开始就使用 TypeScript 构建,对 TypeScript 有着最原生的支持。
    • React: 通过 Create React App (CRA) 的 TypeScript 模板或 Next.js/Gatsby 等框架,可以非常容易地开始使用 TypeScript 开发 React 应用。JSX 语法在 TypeScript 中通过 TSX (.tsx 文件) 得到支持。
    • Vue: Vue CLI 提供了 TypeScript 模板,Vue 3 是使用 TypeScript 编写的,对 TypeScript 的支持也越来越好。
    • Node.js: 使用 ts-node 或将 TypeScript 编译成 JavaScript 后再运行,可以轻松地在 Node.js 环境中开发后端应用。
  • Linter 和 Formatter:
    • ESLint: 结合 @typescript-eslint/parser@typescript-eslint/eslint-plugin,ESLint 可以对 TypeScript 代码进行静态分析,捕获更多潜在错误和代码风格问题。
    • Prettier: 代码格式化工具,能够很好地处理 TypeScript 语法。

第十章:最佳实践与常见陷阱

  • 启用严格模式 (strict: true):tsconfig.json 中启用严格模式是强烈推荐的。它会开启 noImplicitAny, strictNullChecks, strictFunctionTypes, strictPropertyInitialization, noImplicitThis, alwaysStrict 等选项,能捕获更多潜在问题,提高代码健壮性。
  • 尽量避免使用 any: any 会绕过 TypeScript 的类型检查。如果必须处理未知类型,优先考虑使用 unknown,然后通过类型缩小进行安全访问。
  • 接口 vs 类型别名: 当描述对象的形状或类的契约时,优先使用接口。当描述联合类型、交叉类型、原始类型等时,使用类型别名。
  • 理解类型断言的风险: 类型断言是告诉编译器“我知道的比你多”,如果断言错误,运行时会出错。优先使用类型缩小来安全地确定类型。
  • 为函数参数和返回值添加明确的类型注解: 这提高了代码可读性,也让编译器能够进行更全面的检查。
  • 利用 IDE 的类型推断: TypeScript 在很多情况下可以自动推断类型,不需要在每个地方都显式注解。在类型复杂或不明确的地方进行注解即可。
  • 善用类型声明文件 (@types/): 在使用第三方 JavaScript 库时,优先查找并安装其 @types 版本。
  • 持续学习和实践: TypeScript 的类型系统非常强大,也有些复杂。通过阅读官方文档、实践编写代码、参与社区讨论,可以不断提升你的 TypeScript 技能。

结论

TypeScript 通过引入静态类型系统,为 JavaScript 带来了前所未有的健壮性、可维护性和开发者体验。从基本类型到高级泛型,从接口到类,TypeScript 提供了一整套工具来帮助开发者构建复杂且可靠的应用程序。

虽然学习 TypeScript 需要一定的投入,但长远来看,它所带来的收益是巨大的,尤其是在团队协作和大型项目开发中。掌握 TypeScript 不仅能让你写出更少 bug 的代码,更能提升你的开发效率和自信心。

希望这份指南能帮助你踏上或深入探索 TypeScript 之旅。现在就开始将 TypeScript 应用到你的项目中,体验它带来的强大力量吧!

发表评论

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

滚动至顶部