TypeScript 核心概念解析:初学者教程介绍 – wiki基地


TypeScript 核心概念解析:初学者全面教程

在现代 Web 开发领域,JavaScript 无疑是基石。然而,随着项目规模的扩大和复杂性的增加,JavaScript 的动态类型特性有时会成为开发效率和代码质量的瓶颈。为了解决这些问题,微软推出了 TypeScript——一个 JavaScript 的超集,它添加了静态类型系统和其他强大的特性,旨在提高开发大型、复杂应用程序的能力。

对于初学者来说,进入 TypeScript 的世界可能感觉有些 daunting。但别担心,TypeScript 的设计初衷是渐进式的,你可以在现有 JavaScript 项目中逐步引入它。本篇文章将作为你的初学者指南,详细解析 TypeScript 的核心概念,帮助你理解其价值所在,并为你开启 TypeScript 之旅奠定坚实的基础。本文预计篇幅较长,旨在全面覆盖基础知识点,请耐心阅读。

1. 什么是 TypeScript?

核心定义: TypeScript 是 JavaScript 的一个超集(Superset)。这意味着任何有效的 JavaScript 代码也是有效的 TypeScript 代码。TypeScript 在 JavaScript 的基础上,主要增加了静态类型系统和基于类的面向对象编程特性(尽管 ES6 之后 JavaScript 也原生支持类)。

关键点:

  • 静态类型: 与 JavaScript 在运行时(runtime)确定变量类型不同,TypeScript 允许(并鼓励)你在编译时(compile-time)就定义变量、函数参数和返回值的类型。
  • 编译过程: TypeScript 代码不能直接在浏览器或 Node.js 环境中运行。它需要通过 TypeScript 编译器(tsc)转换成纯净、兼容性良好的 JavaScript 代码。这个编译过程也是类型检查发生的地方。
  • 渐进采用: 你不需要一次性重写整个项目。可以在 .js 文件旁边添加 .ts 文件,逐步引入类型检查。

为什么不直接用 JavaScript?
随着应用变得复杂,JavaScript 的动态类型可能导致以下问题:
* 运行时错误: 类型错误(例如,试图对 undefined 调用方法)只有在代码运行时才会被发现。
* 代码可读性和可维护性差: 不明确的类型使得理解函数期望什么输入、返回什么输出变得困难,尤其是在大型团队协作中。
* 重构困难: 没有类型系统的保障,重构代码(如修改函数签名)时更容易引入错误。
* 开发工具支持有限: 虽然现代 IDE 对 JavaScript 的推断能力越来越强,但静态类型能提供更精确、更可靠的代码补全、导航和错误提示。

TypeScript 正是为了解决这些痛点而生。

2. 为什么要使用 TypeScript?

引入 TypeScript 带来的好处是多方面的:

  • 减少 Bug: 静态类型检查能在编码阶段就发现大量的类型相关错误,远早于运行时。这大大减少了调试时间,提高了代码质量。例如,拼写错误的属性名、传递错误类型的参数等问题都能在编译时捕获。
  • 提高代码可读性和可维护性: 类型注解就像代码的文档。开发者可以清晰地看到函数需要什么类型的参数,以及它会返回什么类型的值。这使得代码更容易理解、维护和扩展。
  • 增强的开发工具支持(IDE IntelliSense): 基于明确的类型信息,代码编辑器(如 VS Code)可以提供极其强大的自动补全、代码导航、重构建议和实时错误提示。这极大地提升了开发效率和体验。
  • 更好的团队协作: 类型定义了代码的“契约”。团队成员可以更容易地理解彼此的代码意图,减少沟通成本,确保接口调用的一致性。
  • 面向未来: TypeScript 紧随 ECMAScript 标准,并经常提前支持一些未来的 JavaScript 特性,让你能够使用最新的语言功能,同时编译目标可以设定为兼容旧版浏览器的 JavaScript。
  • 生态系统支持: 越来越多的主流 JavaScript 框架(如 Angular, Vue, React)和库都提供了官方或社区维护的 TypeScript 类型定义文件(.d.ts),使得在项目中使用 TypeScript 变得更加顺畅。

3. TypeScript 环境搭建与编译

安装:
你需要 Node.js 和 npm(或 yarn)环境。通过 npm 全局或项目局部安装 TypeScript:

“`bash

全局安装 (不推荐,除非特定需要)

npm install -g typescript

项目局部安装 (推荐)

npm install –save-dev typescript

或者使用 yarn

yarn add –dev typescript
“`

编译:
假设你有一个 hello.ts 文件:

``typescript
function greet(person: string) {
console.log(
Hello, ${person}!`);
}

greet(“TypeScript User”);
// greet(123); // 编译时会报错: Argument of type ‘number’ is not assignable to parameter of type ‘string’.
“`

使用 TypeScript 编译器 tsc 将其编译为 JavaScript:

“`bash

如果是全局安装

tsc hello.ts

如果是项目局部安装,使用 npx

npx tsc hello.ts
“`

这将生成一个 hello.js 文件,内容大致如下:

javascript
"use strict";
function greet(person) {
console.log("Hello, ".concat(person, "!"));
}
greet("TypeScript User");
// greet(123); // 编译时报错,这一行不会出现在 JS 输出中 (如果编译成功的话)

配置文件 tsconfig.json
对于实际项目,手动编译每个文件很不方便。通常我们会创建一个 tsconfig.json 文件来管理编译选项。在项目根目录下运行 npx tsc --init 可以生成一个带有注释的默认配置文件。

一些重要的配置项:

  • target: 指定编译后输出的 JavaScript 版本(如 “ES5”, “ES2016”, “ESNEXT”)。
  • module: 指定使用的模块系统(如 “CommonJS”, “ES2015”, “ESNext”)。
  • outDir: 指定编译后 JS 文件的输出目录(如 “./dist”)。
  • rootDir: 指定 TypeScript 源文件的根目录(如 “./src”)。
  • strict: 启用所有严格类型检查选项,强烈推荐设置为 true
  • esModuleInterop: 允许更方便地导入 CommonJS 模块,推荐启用。
  • sourceMap: 生成 .map 文件,方便在浏览器中调试原始 TypeScript 代码。
  • include / exclude: 指定哪些文件或目录应该被编译器包含或排除。

有了 tsconfig.json,只需在项目根目录运行 npx tsc,编译器就会根据配置编译整个项目。

4. TypeScript 核心概念详解

4.1 基础类型 (Basic Types)

TypeScript 扩展了 JavaScript 的基本类型,并提供了明确的类型注解。

  • string: 文本类型
    typescript
    let message: string = "Hello, TypeScript!";
  • number: 数字类型(包括整数和浮点数)
    typescript
    let count: number = 42;
    let price: number = 99.99;
  • boolean: 布尔类型(truefalse
    typescript
    let isActive: boolean = true;
  • null: 表示空值 (null)
    typescript
    let data: null = null;
  • undefined: 表示未定义 (undefined)
    typescript
    let notAssigned: undefined = undefined;
    // 注意:在严格空检查(strictNullChecks: true)下,null 和 undefined 只能赋值给它们各自的类型或 void,除非使用联合类型。
  • any: 任意类型。使用 any失去类型检查的优势,应尽量避免使用。它允许你像在纯 JavaScript 中那样操作变量。
    typescript
    let flexible: any = 4;
    flexible = "Now I'm a string";
    flexible = { key: "value" };
    console.log(flexible.nonExistentMethod()); // 编译时不会报错,运行时会出错!
  • unknown: 类型安全的 any。表示一个值的类型未知。在对 unknown 类型的值执行操作前,必须先进行类型检查或类型断言。
    typescript
    let maybe: unknown = 10;
    if (typeof maybe === 'number') {
    let num: number = maybe; // OK: 类型检查后可以赋值
    console.log(num * 2);
    }
    // let fixed: number = maybe; // Error: Type 'unknown' is not assignable to type 'number'.
    // maybe.toFixed(1); // Error: Object is of type 'unknown'.
  • void: 通常用于表示函数没有返回值。
    typescript
    function logMessage(message: string): void {
    console.log(message);
    // return undefined; // 可以,但通常省略
    // return null; // 在 strictNullChecks: false 时可以,但不推荐
    }
  • never: 表示永远不会有返回值的函数的返回类型(例如,总是抛出异常或无限循环的函数)。它也可以表示一个永远不可能发生的类型。
    “`typescript
    function throwError(message: string): never {
    throw new Error(message);
    }

    function infiniteLoop(): never {
    while (true) {}
    }
    * **`object`**: 表示非原始类型(即除了 `string`, `number`, `boolean`, `symbol`, `null`, `undefined`, `bigint` 之外的类型)。typescript
    let obj: object;
    obj = { name: “Alice” };
    obj = [1, 2, 3];
    obj = () => {};
    // obj = 42; // Error
    // obj = “hello”; // Error
    “`

4.2 类型推断 (Type Inference)

TypeScript 的一个强大之处在于它能进行类型推断。如果你在声明变量时立即初始化它,TypeScript 通常能自动推断出变量的类型,你就不需要显式地写类型注解了。

“`typescript
let name = “Bob”; // TypeScript 推断 name 为 string 类型
let age = 30; // TypeScript 推断 age 为 number 类型
let isStudent = false; // TypeScript 推断 isStudent 为 boolean 类型

// name = 100; // Error: Type ‘number’ is not assignable to type ‘string’.
“`

最佳实践是:在变量声明时如果能清晰地初始化,可以让 TypeScript 进行推断。但在函数参数、函数返回值以及无法立即初始化或类型不明显的变量上,最好使用显式类型注解。

4.3 数组 (Arrays)

有两种方式可以定义数组类型:

  1. 元素类型后加 []:
    typescript
    let numbers: number[] = [1, 2, 3, 4, 5];
    let names: string[] = ["Alice", "Bob", "Charlie"];
    // numbers.push("6"); // Error: Argument of type 'string' is not assignable to parameter of type 'number'.
  2. 使用泛型数组类型 Array<元素类型>:
    typescript
    let scores: Array<number> = [100, 95, 88];
    let flags: Array<boolean> = [true, false, true];

    两种方式是等价的,选择哪种取决于个人或团队的偏好。

4.4 元组 (Tuples)

元组允许你表示一个固定数量已知类型的元素的数组。每个位置上的元素类型是确定的。

“`typescript
// 定义一个元组类型,包含一个 string 和一个 number
let person: [string, number];

person = [“Alice”, 30]; // OK
// person = [30, “Alice”]; // Error: Type ‘number’ is not assignable to type ‘string’. Type ‘string’ is not assignable to type ‘number’.
// person = [“Alice”, 30, true]; // Error: Type ‘[string, number, boolean]’ is not assignable to type ‘[string, number]’. Source has 3 element(s) but target allows only 2.

console.log(person[0].substring(1)); // OK, person[0] is known to be a string
console.log(person[1].toFixed(2)); // OK, person[1] is known to be a number
// console.log(person[2]); // Error: Tuple type ‘[string, number]’ of length ‘2’ has no element at index ‘2’.
``
元组在表示有固定结构的数据(如坐标
[number, number]或 key-value 对[string, any]`)时非常有用。

4.5 对象 (Objects)

你可以使用内联类型注解来描述对象的结构:

“`typescript
let user: { name: string; age: number; isAdmin: boolean };

user = {
name: “Bob”,
age: 25,
isAdmin: false,
};

// user.email = “[email protected]”; // Error: Property ’email’ does not exist on type ‘{ name: string; age: number; isAdmin: boolean; }’.
// user.age = “twenty-five”; // Error: Type ‘string’ is not assignable to type ‘number’.
“`

这种方式对于一次性使用的简单对象结构很方便。但对于需要在多处复用的对象结构,使用接口(Interfaces)或类型别名(Type Aliases)是更好的选择。

4.6 接口 (Interfaces)

接口是 TypeScript 的核心概念之一,用于定义对象的形状(Shape)契约(Contract)。它描述了一个对象应该包含哪些属性以及这些属性的类型。

“`typescript
interface Person {
name: string;
age: number;
isAdmin: boolean;
greet?(): void; // 可选方法
readonly country: string; // 只读属性
}

let user1: Person = {
name: “Charlie”,
age: 35,
isAdmin: true,
country: “USA”,
greet() {
console.log(Hello, my name is ${this.name});
}
};

let user2: Person = {
name: “Diana”,
age: 28,
isAdmin: false,
country: “Canada”
// greet 方法是可选的,可以不提供
};

// user1.country = “UK”; // Error: Cannot assign to ‘country’ because it is a read-only property.
user1.age = 36; // OK

function printPersonInfo(person: Person) {
console.log(Name: ${person.name}, Age: ${person.age}, Country: ${person.country});
if (person.greet) {
person.greet();
}
}

printPersonInfo(user1);
printPersonInfo(user2);
“`

接口特性:

  • 可选属性(?: 属性名后的问号表示该属性可以不存在于对象中。
  • 只读属性(readonly: 属性名前加 readonly 关键字,表示该属性在对象创建后不能被修改。
  • 函数类型: 可以描述对象中的方法。
  • 可索引签名: 用于描述可以像数组或字典那样通过索引访问的对象(如 [index: number]: string;)。
  • 接口继承: 一个接口可以继承另一个或多个接口的属性。
    “`typescript
    interface Employee extends Person {
    employeeId: number;
    department: string;
    }

    let emp: Employee = {
    name: “Eve”,
    age: 40,
    isAdmin: false,
    country: “Germany”,
    employeeId: 12345,
    department: “Engineering”
    };
    ``
    * **实现接口 (类)**: 类可以使用
    implements` 关键字来确保它们符合某个接口的结构。

4.7 类型别名 (Type Aliases)

类型别名使用 type 关键字为任何类型(原始类型、联合类型、元组、对象类型、函数类型等)创建一个新的名字。

“`typescript
// 为原始类型创建别名
type UserID = string | number;
let id1: UserID = “user-123”;
let id2: UserID = 456;
// let id3: UserID = false; // Error

// 为对象类型创建别名
type Point = {
x: number;
y: number;
};
let p1: Point = { x: 10, y: 20 };

// 为函数类型创建别名
type MathOperation = (a: number, b: number) => number;
let add: MathOperation = (x, y) => x + y;
let subtract: MathOperation = (x, y) => x – y;

// 结合接口使用
interface Vehicle {
model: string;
}
type Car = Vehicle & { // 使用交叉类型
wheels: number;
};
let myCar: Car = { model: “Tesla Model 3”, wheels: 4 };
“`

接口 vs. 类型别名:

  • 相似点: 多数情况下可以互换使用来定义对象形状。
  • 主要区别:
    • 扩展性: 接口可以使用 extends 关键字继承,并且同名接口会自动合并(声明合并)。类型别名不能被继承或实现(implements),也不能声明合并,但可以通过交叉类型(&)来组合类型。
    • 适用范围: 类型别名更通用,可以为任何类型(包括联合类型、元组等)创建别名,而接口主要用于描述对象或类的结构。
  • 选择:
    • 当定义对象的“契约”或希望类型能够被类实现(implements)或未来可能被扩展(extends)时,优先使用 interface
    • 当需要为联合类型、元组或其他复杂类型(如函数类型、映射类型)创建名称时,或者想使用交叉类型组合现有类型时,使用 type

4.8 函数 (Functions)

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

“`typescript
// 命名函数
function addNumbers(a: number, b: number): number {
return a + b;
}

// 箭头函数
const multiplyNumbers = (a: number, b: number): number => {
return a * b;
};

let result1: number = addNumbers(5, 3); // 8
let result2: number = multiplyNumbers(5, 3); // 15

// console.log(addNumbers(“5”, 3)); // Error: Argument of type ‘string’ is not assignable to parameter of type ‘number’.
“`

函数特性:

  • 返回值类型: 在参数列表后使用 : Type 注明函数期望返回的类型。如果函数没有返回值,使用 : void。如果省略返回值类型,TypeScript 会尝试推断它。
  • 可选参数(?: 在参数名后加问号,表示该参数是可选的。可选参数必须放在必需参数之后。
    typescript
    function greetOptional(name: string, greeting?: string): string {
    return `${greeting || "Hello"}, ${name}!`;
    }
    console.log(greetOptional("Alice")); // "Hello, Alice!"
    console.log(greetOptional("Bob", "Good morning")); // "Good morning, Bob!"
  • 默认参数: 可以为参数提供默认值。带默认值的参数如果出现在必需参数之后,则调用时可以省略;如果出现在必需参数之前,则调用时需要显式传入 undefined 来获取默认值。
    typescript
    function power(base: number, exponent: number = 2): number {
    return Math.pow(base, exponent);
    }
    console.log(power(3)); // 9 (3^2)
    console.log(power(3, 3)); // 27 (3^3)
  • 剩余参数(Rest Parameters): 使用 ... 语法收集函数的多余参数到一个数组中。
    typescript
    function sumAll(...numbers: number[]): number {
    return numbers.reduce((total, num) => total + num, 0);
    }
    console.log(sumAll(1, 2, 3, 4, 5)); // 15
  • 函数类型: 可以定义一个描述函数签名的类型。
    “`typescript
    type StringFormatter = (input: string) => string;

    let toUpperCase: StringFormatter = (str) => str.toUpperCase();
    let addPrefix: StringFormatter = (str) => PREFIX_${str};

    console.log(toUpperCase(“hello”)); // “HELLO”
    console.log(addPrefix(“world”)); // “PREFIX_world”
    “`

4.9 联合类型 (Union Types)

联合类型允许一个变量可以持有多种类型中的一种。使用 |(管道符)分隔各个类型。

“`typescript
let id: string | number;
id = 101; // OK
id = “user-abc”; // OK
// id = true; // Error: Type ‘boolean’ is not assignable to type ‘string | number’.

function printId(inputId: string | number) {
console.log(ID: ${inputId});
// 注意:在操作联合类型变量时,只能访问所有类型共有的属性或方法,
// 或者需要使用类型守卫来缩小类型范围。
if (typeof inputId === “string”) {
// 在这个块内,TypeScript 知道 inputId 是 string 类型
console.log(inputId.toUpperCase());
} else {
// 在这个块内,TypeScript 知道 inputId 是 number 类型
console.log(inputId.toFixed(0));
}
}

printId(200);
printId(“xyz-789”);
“`
联合类型非常适合表示那些可能接受多种输入或返回多种结果的场景。

4.10 交叉类型 (Intersection Types)

交叉类型允许你将多个类型合并为一个类型。这个新类型将拥有所有成员类型的所有属性。使用 & 符号。

“`typescript
interface Draggable {
drag(): void;
}

interface Resizable {
resize(): void;
}

// 交叉类型 Widget 必须同时具有 drag 和 resize 方法
type Widget = Draggable & Resizable;

let textBox: Widget = {
drag: () => console.log(“Dragging the text box…”),
resize: () => console.log(“Resizing the text box…”),
};

textBox.drag();
textBox.resize();
“`
交叉类型常用于将多个接口或类型别名的特性组合在一起。

4.11 枚举 (Enums)

枚举(enum)允许你定义一组命名的常量。它可以使代码更具可读性,替代难以理解的“魔术数字”或字符串。

  • 数字枚举: 默认情况下,枚举成员从 0 开始自动递增赋值。
    “`typescript
    enum Direction {
    Up, // 0
    Down, // 1
    Left, // 2
    Right, // 3
    }
    let move: Direction = Direction.Up;
    console.log(move); // 输出 0

    // 也可以手动指定值
    enum Status {
    Pending = 1,
    Processing = 2,
    Success = 3,
    Failed = 4,
    }
    let currentStatus: Status = Status.Processing;
    console.log(currentStatus); // 输出 2
    * **字符串枚举**: 每个成员都需要显式地用字符串字面量初始化。字符串枚举没有反向映射。typescript
    enum LogLevel {
    Info = “INFO”,
    Warning = “WARN”,
    Error = “ERROR”,
    }
    let level: LogLevel = LogLevel.Warning;
    console.log(level); // 输出 “WARN”
    “`
    * 异构枚举: 不推荐,但允许混合字符串和数字成员。

枚举主要用于定义一组有限的、相关的常量值集合。

4.12 泛型 (Generics)

泛型是 TypeScript 中非常强大的特性,它允许你编写可重用的代码组件,这些组件可以处理多种类型而不是单一类型。泛型在创建灵活的数据结构(如数组、列表)、函数或类时非常有用。

动机:
假设你想写一个函数,它接收一个参数并返回该参数。如果不用泛型,你可能需要为每种类型写一个版本,或者使用 any(丢失类型信息)。

“`typescript
// 不好的做法:使用 any
function identityAny(arg: any): any {
return arg;
}
let outputAny = identityAny(“myString”); // outputAny 是 any 类型,失去了 string 的类型信息
let outputNumAny = identityAny(123); // outputNumAny 也是 any

// 使用泛型
function identity(arg: T): T {
return arg;
}

// 调用泛型函数
let outputString = identity(“myString”); // 显式指定 T 为 string
let outputNumber = identity(123); // TypeScript 类型推断 T 为 number

console.log(outputString.toUpperCase()); // OK, outputString 是 string 类型
console.log(outputNumber.toFixed(2)); // OK, outputNumber 是 number 类型
“`

identity<T> 中,T 是一个类型变量,它代表了调用函数时将传入的实际类型。这使得函数 identity 可以安全地处理任何类型,同时保留了输入和输出之间的类型关系。

泛型应用:

  • 泛型函数: 如上例所示。
  • 泛型接口:
    typescript
    interface Box<T> {
    value: T;
    }
    let stringBox: Box<string> = { value: "content" };
    let numberBox: Box<number> = { value: 100 };
  • 泛型类:
    “`typescript
    class GenericNumber {
    zeroValue: T;
    add: (x: T, y: T) => T;

    constructor(zero: T, addFunc: (x: T, y: T) => T) {
    this.zeroValue = zero;
    this.add = addFunc;
    }
    }
    let myNumber = new GenericNumber(0, (x, y) => x + y);
    let myString = new GenericNumber(“”, (x, y) => x + y);

    console.log(myNumber.add(5, 10)); // 15
    console.log(myString.add(“Hello, “, “World!”)); // “Hello, World!”
    * **泛型约束**: 有时你需要确保泛型类型 `T` 具有某些属性或方法。可以使用 `extends` 关键字添加约束。typescript
    interface Lengthwise {
    length: number;
    }

    function logLength(arg: T): void {
    console.log(arg.length); // OK, 因为 T 被约束为必须有 length 属性
    }

    logLength(“hello”); // string 有 length 属性
    logLength([1, 2, 3]); // array 有 length 属性
    logLength({ length: 10, value: 3 }); // 对象有 length 属性
    // logLength(123); // Error: Argument of type ‘number’ is not assignable to parameter of type ‘Lengthwise’.
    “`

泛型是编写高度可复用、类型安全代码的关键。

4.13 类 (Classes)

TypeScript 在 ES6 类的基础上添加了类型注解和访问修饰符。

“`typescript
class Animal {
// 属性类型注解
public name: string; // public 是默认的,可以省略
private age: number; // private 成员只能在类内部访问
protected species: string; // protected 成员可以在类及其子类中访问

// 构造函数参数属性 (Parameter Properties)
// 一种简写,直接在构造函数参数前加修饰符,会自动创建同名属性并赋值
constructor(name: string, age: number, species: string, readonly id: number) {
this.name = name;
this.age = age;
this.species = species;
// id 是只读的,在构造函数中赋值后不能修改
}

// 方法类型注解
public move(distanceInMeters: number = 0): void {
console.log(${this.name} moved ${distanceInMeters}m.);
this.logAge(); // 可以访问 private 成员
}

private logAge(): void {
console.log(${this.name}'s age is ${this.age});
}

protected getSpecies(): string {
return this.species;
}
}

class Dog extends Animal {
public breed: string;

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

public bark(): void {
console.log(“Woof! Woof!”);
console.log(Species: ${this.species}); // 可以访问 protected 成员
// console.log(Age: ${this.age}); // Error: Property ‘age’ is private and only accessible within class ‘Animal’.
}

// 重写父类方法
public move(distanceInMeters: number = 5): void {
console.log(“Running…”);
super.move(distanceInMeters); // 调用父类方法
}
}

let fluffy = new Dog(“Fluffy”, 3, “Golden Retriever”, 1);
fluffy.move(10);
fluffy.bark();
console.log(fluffy.name); // OK (public)
// console.log(fluffy.age); // Error (private)
// console.log(fluffy.species); // Error (protected, 只能在类和子类内部访问)
console.log(fluffy.id); // OK (readonly public)
// fluffy.id = 2; // Error (readonly)
“`

关键点:

  • 访问修饰符: public(默认,随处可访问)、private(仅类内部可访问)、protected(类及子类内部可访问)。
  • readonly 修饰符: 用于属性,确保属性只能在构造函数中初始化。
  • 构造函数参数属性: 简化属性声明和初始化的语法糖。
  • 继承 (extends): 子类继承父类的属性和方法。
  • super(): 在子类构造函数中调用父类构造函数。
  • 抽象类 (abstract): 不能被实例化的基类,可以包含抽象方法(没有实现体,必须在子类中实现)。

4.14 模块 (Modules)

TypeScript 采用 ES6 模块标准(import/export)。任何包含顶级 importexport 语句的文件都被视为一个模块。

“`typescript
// — 文件: mathUtils.ts —
export const PI = 3.14159;

export function add(a: number, b: number): number {
return a + b;
}

export class Calculator {
multiply(a: number, b: number): number {
return a * b;
}
}

// — 文件: main.ts —
import { PI, add, Calculator } from “./mathUtils”;
// 也可以导入整个模块
import * as math from “./mathUtils”;

console.log(PI); // 3.14159
console.log(add(2, 3)); // 5

const calc = new Calculator();
console.log(calc.multiply(4, 5)); // 20

console.log(math.PI); // 3.14159
const calc2 = new math.Calculator();
console.log(calc2.multiply(6, 7)); // 42
“`

模块化有助于组织代码、避免命名冲突,并创建可重用的代码库。TypeScript 编译器(根据 tsconfig.json 中的 module 选项)会根据目标环境将 ES6 模块语法转换为相应的模块格式(如 CommonJS, AMD, UMD, or 保留为 ES6 模块)。

5. 类型断言 (Type Assertions)

有时,你可能比 TypeScript 编译器更了解某个值的具体类型。在这种情况下,你可以使用类型断言来“告诉”编译器:“相信我,我知道这个值的类型。”

类型断言有两种语法:

  1. 尖括号语法: <Type>value
  2. as 语法: value as Type (在 JSX/TSX 文件中,只能使用 as 语法以避免与 JSX 标签冲突)

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

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

// as 语法 (更推荐,尤其是在 React 项目中)
let strLength2: number = (someValue as string).length;

console.log(strLength1); // 16
console.log(strLength2); // 16

// 错误使用:如果断言的类型不合理,编译时可能不报错,但运行时会出错
let numValue: unknown = 123;
// let wrongLength: number = (numValue as string).length; // 运行时会抛错,因为 number 没有 length 属性
“`

注意: 类型断言不会在运行时进行任何特殊的检查或数据转换。它仅仅是在编译时影响类型检查。如果你的断言是错误的,代码在运行时可能会失败。因此,只有在你确实确定类型的情况下才应使用类型断言,并且优先考虑使用类型守卫(如 typeof, instanceof, 或自定义类型保护函数)来进行安全的类型缩小。

6. 结语与下一步

恭喜你!你已经初步了解了 TypeScript 的核心概念,包括它的基本原理、主要优势、环境设置,以及最重要的类型系统特性:基础类型、接口、类型别名、函数类型、泛型、类、模块等等。

TypeScript 的学习曲线是渐进的。你不需要一开始就掌握所有高级特性。从给现有的 JavaScript 代码添加类型注解开始,逐步利用接口、泛型等工具来构建更健壮、更易于维护的应用程序。

下一步建议:

  1. 动手实践: 理论学习固然重要,但编程能力的提升离不开实践。尝试在你自己的小项目或练习中使用 TypeScript。
  2. 深入学习 tsconfig.json 了解更多编译选项,特别是 strict 模式下的各个选项,它们能帮助你写出更严谨的 TypeScript 代码。
  3. 探索高级类型: 学习条件类型(Conditional Types)、映射类型(Mapped Types)、模板字面量类型(Template Literal Types)等,它们能让你以更强大的方式操作类型。
  4. 类型定义文件 (.d.ts): 学习如何为没有原生 TypeScript 支持的 JavaScript 库编写或使用类型定义文件。
  5. 结合框架使用: 如果你使用 React, Vue, Angular 等框架,学习如何在这些框架中最佳地实践 TypeScript。
  6. 查阅官方文档: TypeScript 官方文档是学习和参考的最佳资源,内容详尽且更新及时。

拥抱 TypeScript 可能是在现代 Web 开发中提升代码质量和开发效率最有效的方式之一。虽然初期需要投入一些学习成本来理解类型系统,但长远来看,它带来的代码健壮性、可维护性和开发体验的提升是无价的。希望这篇详细的初学者教程能为你打下坚实的基础,祝你在 TypeScript 的世界里探索愉快!


发表评论

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

滚动至顶部