拥抱更可靠的代码:一份全面的 TypeScript 入门与实践教程
作为一门构建于 JavaScript 之上的语言,TypeScript 在近年来受到了广泛关注和应用。它为 JavaScript 带来了静态类型检查、强大的工具支持以及更好的代码组织能力,极大地提升了大型应用开发的可维护性和可靠性。如果你是一名 JavaScript 开发者,想要提升自己的开发效率和代码质量,那么学习 TypeScript 绝对是一个明智的选择。
本文将带你从零开始,深入了解 TypeScript 的核心概念、基本语法以及如何在实际项目中应用它。无论你是刚接触 TypeScript 的新手,还是想系统性梳理知识点的开发者,这篇教程都将为你提供一份详尽的指南。
目录
-
为什么选择 TypeScript?
- JavaScript 的“痛点”
- TypeScript 如何解决问题
- 主要优势:静态类型、强大的工具、可维护性
-
安装与设置
- Node.js 与 npm/yarn/pnpm
- 安装 TypeScript 编译器
- 第一个 TypeScript 文件:
hello.ts
- 编译 TypeScript 代码
tsconfig.json
配置文件简介
-
TypeScript 基础类型
- 布尔值(
boolean
) - 数字(
number
) - 字符串(
string
) - 数组(
Array
或[]
) - 元组(
Tuple
) - 枚举(
enum
) any
类型:慎用!void
类型null
和undefined
never
类型object
类型
- 布尔值(
-
变量与函数类型注解
- 变量类型注解
- 函数参数类型注解
- 函数返回值类型注解
- 可选参数与默认参数
- 剩余参数
- 函数重载
-
接口(Interfaces)
- 什么是接口?
- 定义对象形状
- 可选属性与只读属性
- 函数类型接口
- 可索引签名
- 扩展接口
- 接口实现类
-
类(Classes)
- ES6 Class 的基础上增强
- 属性与方法
- 构造函数
- 继承
- 修饰符(
public
,private
,protected
,readonly
) - 抽象类与抽象方法
-
类型别名(Type Aliases)
- 使用
type
关键字 - 与接口的区别和选择
- 使用
-
联合类型(Union Types)与交叉类型(Intersection Types)
- 联合类型(
|
):表示值可以是多种类型之一 - 交叉类型(
&
):合并多种类型为一个新类型
- 联合类型(
-
字面量类型(Literal Types)
- 将特定字符串、数字或布尔值作为类型
-
类型断言(Type Assertions)
- 告诉编译器你比它更了解类型
- 两种语法:
as
关键字和<>
语法 - 使用场景与注意事项
-
泛型(Generics)
- 为什么需要泛型?
- 泛型函数
- 泛型接口
- 泛型类
- 泛型约束
-
模块(Modules)
- 使用
import
和export
- 使用
-
与 JavaScript 代码的协同
- 声明文件(
.d.ts
) - 使用
@types
库
- 声明文件(
-
深入
tsconfig.json
- 文件结构与常用选项详解
strict
模式:为什么强烈推荐开启target
,module
,outDir
,rootDir
等
-
TypeScript 在现代前端/后端框架中的应用(简述)
- React, Vue, Angular
- Node.js (Express, Koa, NestJS)
-
总结与下一步
1. 为什么选择 TypeScript?
JavaScript 是一门灵活、动态的语言,这使得它非常易于上手和快速开发原型。然而,当项目规模变大、团队成员增多时,JavaScript 的动态特性也带来了一些挑战:
- 运行时错误: 很多类型错误或属性访问错误只有在代码运行时才会暴露,增加了调试成本。
- 代码可读性与理解难度: 变量和函数没有明确的类型信息,需要通过阅读代码或文档来推断其预期的数据结构,增加了理解成本。
- 重构困难: 修改代码时,很难确定一个改动会影响到哪些部分,容易引入新的 bug。
- 工具支持受限: 编辑器和 IDE 难以提供精确的代码补全、导航和重构功能。
TypeScript(简称 TS)正是一种旨在解决这些问题的语言。它是 JavaScript 的一个超集,这意味着任何合法的 JavaScript 代码也是合法的 TypeScript 代码。TypeScript 在 JavaScript 的基础上添加了静态类型系统。
静态类型意味着在代码编译阶段(而不是运行阶段)就可以检查类型错误。当你编写 TypeScript 代码时,TypeScript 编译器(tsc
)会分析你的代码,并在发现类型不匹配或其他潜在问题时立即报错。
TypeScript 如何解决问题?
- 提前发现错误: 大部分运行时错误在编译阶段就被捕获。
- 提高代码可读性: 类型注解提供了代码的“契约”或“蓝图”,清晰地表明了变量、函数、对象等预期的结构。
- 增强可维护性与重构安全性: 修改代码时,编译器会提示哪些地方因为类型不匹配而需要调整,大大降低了引入 bug 的风险。
- 强大的工具支持: 基于类型信息,编辑器(如 VS Code, WebStorm)可以提供智能的代码补全、参数提示、错误检查、定义跳转、自动重构等功能,极大地提升开发效率。
主要优势总结:
- 提高代码质量和可靠性: 静态类型检查是核心优势。
- 提升开发效率: 得益于优秀的编辑器工具支持。
- 改善团队协作: 类型信息作为一种契约,使得团队成员更容易理解和使用彼此的代码。
- 拥抱最新 JavaScript 特性: TypeScript 支持最新的 ECMAScript 标准,并可以编译到旧版本的 JavaScript,解决兼容性问题。
2. 安装与设置
学习 TypeScript 的第一步是安装 TypeScript 编译器。
Node.js 与 npm/yarn/pnpm
TypeScript 编译器是一个 Node.js 包,因此你需要先安装 Node.js 环境。安装 Node.js 后,你将获得 npm(Node 包管理器)。或者你也可以选择使用 yarn 或 pnpm 作为包管理器。
安装 TypeScript 编译器
打开你的终端或命令行工具,运行以下命令全局安装 TypeScript:
bash
npm install -g typescript
或者使用 yarn:
bash
yarn global add typescript
安装完成后,你可以通过以下命令检查安装是否成功以及 TypeScript 的版本:
bash
tsc --version
第一个 TypeScript 文件:hello.ts
在你的代码编辑器中创建一个新文件,命名为 hello.ts
。输入以下简单的 TypeScript 代码:
“`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)); // Argument of type ‘number’ is not assignable to parameter of type ‘string’.
“`
注意 greet
函数中的 : string
,这就是一个类型注解,表明 person
参数期望一个字符串类型的值。
编译 TypeScript 代码
TypeScript 代码不能直接在浏览器或 Node.js 中运行(除非使用特定的运行时,如 ts-node
)。它需要被编译成普通的 JavaScript 代码。使用 tsc
命令来编译你的 .ts
文件:
bash
tsc hello.ts
执行这个命令后,如果代码没有类型错误,编译器会在同一目录下生成一个同名的 JavaScript 文件:hello.js
。
javascript
// hello.js (由 tsc hello.ts 生成)
function greet(person) {
return "Hello, " + person;
}
var user = "TypeScript User";
console.log(greet(user));
// 尝试传入错误类型的数据 (这将导致编译错误)
// let num = 123;
// console.log(greet(num)); // Argument of type 'number' is not assignable to parameter of type 'string'.
可以看到,类型注解 : string
在编译后的 JavaScript 代码中被移除了,因为 JavaScript 本身不支持静态类型。
你现在可以使用 Node.js 运行生成的 hello.js
文件:
bash
node hello.js
输出应该是:Hello, TypeScript User
。
如果你取消注释 greet(num)
那一行,并再次运行 tsc hello.ts
,编译器会在编译阶段报错,并告诉你 number
类型不能赋值给 string
类型,阻止你生成可能在运行时出错的 JavaScript 文件。
tsconfig.json
配置文件简介
对于大型项目,手动编译每个文件或使用复杂的命令行参数是不现实的。tsconfig.json
文件是 TypeScript 项目的配置文件,它告诉编译器如何编译项目中的 .ts
文件。
在项目根目录运行以下命令可以生成一个基本的 tsconfig.json
文件:
bash
tsc --init
这会生成一个包含许多选项的 tsconfig.json
文件,大部分初始状态是被注释掉的。你可以根据需要启用或修改这些选项。例如:
``json
checkJS
// 部分 tsconfig.json 内容
{
"compilerOptions": {
/* Language and Environment Options */
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
"module": "commonjs", /* Specify what module code is generated. */
// "lib": [], /* Specify a set of bundled library declaration files that describe the target environment. */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use theoption to get errors from these files. */
allowSyntheticDefaultImports` for type compatibility. */
// "checkJs": true, /* Enable error reporting in .js files. */
"strict": true, /* Enable all strict type-checking options. */
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables
/* Emit */
// "outDir": "./dist", /* Specify an output folder for all emitted files. */
// "rootDir": "./src", /* Specify the root folder within your source files. */
/* Type Checking */
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type. */
// "strictNullChecks": true, /* When enabled, `null` and `undefined` are not in the domain of every type and need to be explicitly included. */
// ...更多选项
},
// “include”: [“src//“], / Specify an array of file patterns to include in the program. */
// “exclude”: [“node_modules”, “/.spec.ts”] / Specify an array of file patterns to exclude from the program. */
}
“`
有了 tsconfig.json
文件后,只需在项目根目录运行 tsc
命令,编译器就会查找该文件并按照其中的配置来编译项目中的所有 TypeScript 文件。
3. TypeScript 基础类型
TypeScript 支持与 JavaScript 几乎相同的数据类型,并在此基础上添加了类型注解的能力。
-
布尔值(
boolean
)最基本的数据类型,只有
true
和false
。typescript
let isDone: boolean = false;
let isComplete = true; // 类型推断为 boolean -
数字(
number
)所有数字都是浮点数,支持十进制、十六进制、二进制和八进制字面量。
typescript
let decimal: number = 6;
let hex: number = 0xf00d;
let binary: number = 0b1010;
let octal: number = 0o744;
let big: number = 100_000_000; // 数字分隔符 -
字符串(
string
)表示文本数据。可以使用单引号、双引号或模板字符串。
typescript
let color: string = "blue";
let myName = 'Alice'; // 类型推断为 string
let sentence: string = `Hello, my name is ${myName}.`; // 模板字符串 -
数组(
Array
或[]
)有两种方式表示数组:
-
元素类型后面接
[]
:typescript
let list: number[] = [1, 2, 3]; -
使用泛型
Array<元素类型>
:typescript
let list2: Array<number> = [1, 2, 3];
数组中的所有元素都必须是指定的类型。
-
-
元组(
Tuple
)元组类型允许你表示一个已知元素数量和类型的数组,各元素的类型不必相同。
typescript
// 定义一个元组,第一个元素是 string,第二个元素是 number
let x: [string, number];
// 初始化它
x = ['hello', 10]; // OK
// x = [10, 'hello']; // 错误:类型不匹配
console.log(x[0].substring(1)); // OK,知道 x[0] 是 string
// console.log(x[1].substring(1)); // 错误:知道 x[1] 是 number访问越界的元素会使用联合类型,通常是元组中所有类型的联合,这通常是不安全的,开启
strict
模式下会阻止这种行为。 -
枚举(
enum
)enum
类型是对 JavaScript 标准数据类型的一个补充。它允许你定义一组命名的常量。“`typescript
enum Color {Red, Green, Blue}
let c: Color = Color.Green; // 默认情况下,从 0 开始为元素编号console.log(c); // 输出 1
// 你也可以手动指定成员的值
enum Direction {Up = 1, Down, Left, Right}
let d: Direction = Direction.Up; // d 的值是 1
console.log(d); // 输出 1
console.log(Direction.Down); // 输出 2// 或者全部手动指定
enum FileAccess {
None,
Read = 1 << 1, // 位移运算符
Write = 1 << 2,
ReadWrite = Read | Write,
// …
}// 基于值获取成员名
let colorName: string = Color[2];
console.log(colorName); // 输出 “Blue”
“`枚举在需要表示一组有限的、有意义的数值集合时非常有用。
-
any
类型:慎用!当你不知道变量会是什么类型,或者需要处理来自外部(如用户输入、第三方库)的动态内容时,可以使用
any
类型。any
类型可以绕过类型检查,允许你访问其任意属性或调用任意方法。“`typescript
let notSure: any = 4;
notSure = “maybe a string instead”;
notSure = false; // booleanlet list: any[] = [1, true, “free”];
list[1] = 100;
“`警告: 使用
any
会丢失 TypeScript 提供的类型检查优势。你应该尽量避免使用any
,除非你知道自己在做什么,或者在迁移旧的 JavaScript 项目时作为过渡。如果可能,尝试使用更具体的类型,如unknown
(更安全,后面会提到)或联合类型。 -
void
类型void
表示没有任何类型。通常用作函数没有返回值的类型。“`typescript
function warnUser(): void {
console.log(“This is a warning message”);
}let unusable: void = undefined; // 在 strictNullChecks: false 的情况下,可以赋值 null 或 undefined
// let unusable: void = null; // 在 strictNullChecks: false 的情况下
// let unusable: void = 1; // 错误
“` -
null
和undefined
默认情况下,
null
和undefined
是所有类型的子类型。这意味着你可以将null
或undefined
赋值给number
或string
等类型。“`typescript
let u: undefined = undefined;
let n: null = null;// let num: number = undefined; // 在 strictNullChecks: false 的情况下 OK
// let str: string = null; // 在 strictNullChecks: false 的情况下 OK
“`然而,当你在
tsconfig.json
中启用strictNullChecks: true
(强烈推荐)时,null
和undefined
只能赋值给它们自身的类型以及void
或any
。这有助于防止常见的运行时错误(如访问null
或undefined
的属性)。“`typescript
// 在 strictNullChecks: true 的情况下
let num: number = 1;
// num = undefined; // 错误
// num = null; // 错误let str: string | null = “hello”; // 必须明确联合类型
str = null; // OK
“` -
never
类型never
类型表示那些永不存在的值的类型。例如,总是抛出错误的函数表达式或箭头函数表达式的返回值类型,或者在类型守卫中被永不满足的类型。“`typescript
// 返回 never 的函数必须包含无法到达的终点
function error(message: string): never {
throw new Error(message);
}// 推断的返回值类型为 never
function fail() {
return error(“Something failed”);
}// 返回 never 的函数示例:一个无限循环
function infiniteLoop(): never {
while (true) {
}
}
“`never
是所有类型的子类型,但没有任何类型是never
的子类型(除了never
本身)。这使得never
在处理穷尽检查时非常有用。 -
object
类型object
表示非原始类型,也就是除number
,string
,boolean
,symbol
,null
,undefined
之外的类型。“`typescript
function create(o: object | null): void {
// …
}create({ prop: 0 }); // OK
create(null); // OK
// create(42); // 错误
// create(“string”); // 错误
// create(false); // 错误
// create(undefined); // 错误 (除非 strictNullChecks: false)
“`
4. 变量与函数类型注解
类型注解是 TypeScript 最显眼的功能之一。它使用 : type
的语法来指定变量、函数参数和函数返回值的类型。
-
变量类型注解
在变量声明后使用
: type
:typescript
let age: number = 30;
let name: string = "Bob";
let isActive: boolean = true;
let hobbies: string[] = ["reading", "gaming"];
let person: { name: string, age: number } = { name: "Charlie", age: 25 };类型推断: 在很多情况下,TypeScript 可以根据变量的初始值自动推断出其类型,这时你就不需要显式地写出类型注解。这被称为类型推断。
typescript
let count = 10; // 推断为 number
let greeting = "Hello"; // 推断为 string
let isVisible = false; // 推断为 boolean尽管如此,对于函数的参数、返回值以及结构复杂的对象,显式地添加类型注解可以提高代码的可读性,并作为一种明确的文档。
-
函数参数类型注解
在函数参数名后使用
: type
:“`typescript
function add(a: number, b: number): number {
return a + b;
}// add(5, “10”); // 错误:Argument of type ‘string’ is not assignable to parameter of type ‘number’.
add(5, 10); // OK
“` -
函数返回值类型注解
在函数参数列表的圆括号后使用
: type
:“`typescript
function greet(name: string): string {
return “Hello, ” + name;
}function logMessage(message: string): void { // 没有返回值,使用 void
console.log(message);
}// 函数表达式也可以
const subtract = (a: number, b: number): number => a – b;
“`TypeScript 也可以通过返回值推断出函数的返回类型,但显式声明返回值类型是一个很好的实践,尤其是在函数体比较复杂时。
-
可选参数与默认参数
在参数名后使用
?
表示可选参数:“`typescript
function buildName(firstName: string, lastName?: string): string {
if (lastName) {
return firstName + ” ” + lastName;
} else {
return firstName;
}
}console.log(buildName(“Bob”)); // OK
console.log(buildName(“Bob”, “Adams”)); // OK
// console.log(buildName(“Bob”, “Adams”, “Sr.”)); // 错误:参数过多
“`为参数提供默认值时,该参数也会变成可选参数:
“`typescript
function buildNameWithDefault(firstName: string, lastName: string = “Smith”): string {
return firstName + ” ” + lastName;
}console.log(buildNameWithDefault(“Bob”)); // OK, lastName becomes “Smith”
console.log(buildNameWithDefault(“Bob”, “Adams”)); // OK, lastName is “Adams”
“` -
剩余参数(Rest Parameters)
当不知道函数会有多少个参数时,可以使用剩余参数,它是一个数组:
“`typescript
function sum(…numbers: number[]): number {
return numbers.reduce((total, num) => total + num, 0);
}console.log(sum(1, 2, 3)); // 输出 6
console.log(sum(1, 2, 3, 4, 5)); // 输出 15
“` -
函数重载(Function Overloads)
函数重载允许你为同一个函数提供多个函数类型定义,根据传入的参数数量或类型不同,执行不同的逻辑或具有不同的返回类型。
你需要先定义一系列重载签名(只有参数和返回值类型,没有函数体),最后再定义一个实现签名(包含函数体,通常使用联合类型或 any 来兼容所有重载签名)。
“`typescript
// 重载签名
function pickCard(x: number): number; // 输入数字,返回数字
function pickCard(x: number[]): number; // 输入数字数组,返回数字
function pickCard(x: string): string; // 输入字符串,返回字符串// 实现签名 (必须兼容所有重载签名)
function pickCard(x: number | number[] | string): number | string {
// 在这里实现函数逻辑
if (typeof x === “number”) {
return x * 2; // 假设返回一个数字
} else if (Array.isArray(x)) {
return x.length; // 假设返回数组长度
} else if (typeof x === “string”) {
return x.toUpperCase(); // 假设返回大写字符串
}
// 注意:实现签名本身不能被外部直接调用,类型检查只针对重载签名
// 为了让编译器满意,可能需要处理所有分支或抛出错误
throw new Error(“Invalid argument type”);
}console.log(pickCard(10)); // 根据第一个签名,返回值推断为 number (实际输出 20)
console.log(pickCard([1, 2, 3])); // 根据第二个签名,返回值推断为 number (实际输出 3)
console.log(pickCard(“hello”)); // 根据第三个签名,返回值推断为 string (实际输出 “HELLO”)// pickCard(true); // 错误:没有匹配的重载签名
“`
函数重载主要用于提供更好的类型提示和检查,它并不会在运行时真正“重载”函数。
5. 接口(Interfaces)
接口是 TypeScript 中非常核心的概念,它用于定义对象的形状(Shape)。接口可以看作是一种契约,规定了实现该接口的对象必须包含哪些属性和方法及其类型。
-
什么是接口?
想象一下,你有一个函数需要一个配置对象作为参数,这个对象应该有哪些属性?每个属性的类型是什么?接口就是用来描述这个“应该有的形状”。
“`typescript
interface LabeledValue {
label: string;
}function printLabel(labeledObj: LabeledValue) {
console.log(labeledObj.label);
}let myObj = { size: 10, label: “Size 10 Object” };
printLabel(myObj); // OK,myObj 的形状符合 LabeledValue 接口
“`myObj
即使有size
属性,只要它包含了label
属性且类型为string
,就符合LabeledValue
接口的要求。 -
定义对象形状
接口可以定义更复杂的对象形状,包括各种属性和方法。
“`typescript
interface Person {
name: string;
age: number;
isStudent: boolean;
address?: string; // 可选属性
readonly id: number; // 只读属性
greet(message: string): void; // 方法签名
}let user: Person = {
id: 123,
name: “Alice”,
age: 30,
isStudent: false,
greet: function(message: string) {
console.log(${this.name} says: ${message}
);
}
};console.log(user.name);
// user.id = 456; // 错误:id 是只读属性
user.greet(“Hello!”);// 如果缺少必须属性会报错
// let anotherUser: Person = { name: “Bob”, age: 25 }; // 错误:缺少 isStudent 和 greet 属性
“` -
可选属性与只读属性
- 可选属性 (
?
):表示该属性可有可无。 - 只读属性 (
readonly
):表示属性在对象创建后就不能被修改。
- 可选属性 (
-
函数类型接口
接口也可以用来描述函数的类型。
“`typescript
interface SearchFunc {
(source: string, subString: string): boolean;
}let mySearch: SearchFunc;
mySearch = function(src: string, sub: string): boolean {
let result = src.search(sub);
return result > -1;
};// mySearch = function(src: string, sub: number): boolean { … } // 错误:参数类型不匹配
“` -
可索引签名
描述那些可以通过索引来访问元素的对象,比如数组或字典(key-value pair)。
“`typescript
interface StringArray {
[index: number]: string; // 数字索引,值是 string
}let myArray: StringArray;
myArray = [“Bob”, “Fred”];console.log(myArray[0]); // OK
interface Dictionary {
[key: string]: number; // 字符串索引,值是 number
length: number; // 可以同时有其他属性,但其类型必须兼容索引签名的类型
}let myDictionary: Dictionary = { “apple”: 1, “banana”: 2, length: 2 };
// let badDictionary: Dictionary = { “apple”: 1, “banana”: “two” }; // 错误:值类型不匹配
“`需要注意的是,如果同时使用数字索引和字符串索引,数字索引的返回值类型必须是字符串索引返回值类型的子类型。这是因为 JavaScript 会将数字索引转换为字符串(
obj[1]
相当于obj['1']
)。 -
扩展接口
接口可以继承其他接口,创建更复杂的接口。
“`typescript
interface Shape {
color: string;
}interface Square extends Shape {
sideLength: number;
}let square: Square = {
color: “blue”,
sideLength: 10
};// 接口可以继承多个接口
interface PenStroke {
penWidth: number;
}interface SquareWithPen extends Square, PenStroke {
// 同时拥有 color, sideLength, penWidth
}let newSquare: SquareWithPen = {
color: “red”,
sideLength: 5,
penWidth: 2
};
“` -
接口实现类
类可以使用
implements
关键字来实现一个或多个接口,确保类符合接口定义的契约。“`typescript
interface Clock {
currentTime: Date;
setTime(d: Date): void;
}class DigitalClock implements Clock {
currentTime: Date = new Date();
setTime(d: Date) {
this.currentTime = d;
}
constructor(h: number, m: number) {
// … constructor logic
}
}// 类也可以实现多个接口
interface Alarm {
alarmTime: Date;
}class CalendarAlarmClock implements Clock, Alarm {
currentTime: Date = new Date();
alarmTime: Date = new Date(); // 必须实现 Alarm 接口的属性setTime(d: Date) { // 必须实现 Clock 接口的方法 this.currentTime = d; } constructor(date: Date) { this.currentTime = date; this.alarmTime = date; // 设置默认闹钟时间 } // ... 其他属性和方法
}
“`
6. 类(Classes)
TypeScript 在 ECMAScript 2015(ES6)的 Class 语法基础上进行了增强,加入了强大的类型系统和访问修饰符。
-
属性与方法
“`typescript
class Greeter {
greeting: string; // 属性声明constructor(message: string) { // 构造函数 this.greeting = message; } greet(): string { // 方法 return "Hello, " + this.greeting; }
}
let greeter = new Greeter(“world”);
console.log(greeter.greet()); // 输出 “Hello, world”
“` -
继承
使用
extends
关键字实现类的继承。派生类(子类)继承基类(父类)的属性和方法。在派生类的构造函数中,必须调用super()
来调用基类的构造函数。``typescript
${this.name} moved ${distanceInMeters}m.`);
class Animal {
name: string;
constructor(theName: string) { this.name = theName; }
move(distanceInMeters: number = 0) {
console.log(
}
}class Snake extends Animal {
constructor(name: string) { super(name); } // 调用父类构造函数
move(distanceInMeters = 5) { // 重写 move 方法
console.log(“Slithering…”);
super.move(distanceInMeters); // 调用父类的 move 方法
}
}class Horse extends Animal {
constructor(name: string) { super(name); }
move(distanceInMeters = 45) { // 重写 move 方法
console.log(“Galloping…”);
super.move(distanceInMeters); // 调用父类的 move 方法
}
}let sam = new Snake(“Sammy the Python”);
let tom: Animal = new Horse(“Tommy the Palomino”); // 可以将子类实例赋值给父类类型的变量sam.move();
tom.move(34);
“` -
修饰符(
public
,private
,protected
,readonly
)TypeScript 为类成员提供了访问修饰符,控制属性和方法的可访问性。
public
(默认):在任何地方都可以访问。private
:只能在定义它的类内部访问。protected
:可以在定义它的类内部以及继承它的子类中访问。readonly
:属性只能在声明时或构造函数中被初始化,之后不能修改。
“`typescript
class Department {
private employees: string[] = []; // private 只能在 Department 内部访问constructor(public name: string) { // public 作为构造函数参数,会自动创建并初始化同名 public 属性 } addEmployee(employee: string) { this.employees.push(employee); } printEmployeeNames() { console.log(this.employees); }
}
let accounting = new Department(“Accounting”);
accounting.addEmployee(“Bob”);
accounting.addEmployee(“Anna”);
accounting.printEmployeeNames(); // 输出 [“Bob”, “Anna”]// console.log(accounting.employees); // 错误:employees 是 private
class ITDepartment extends Department {
public admins: string[] = [];
private lastReport: string; // private 属性constructor(name: string, admins: string[]) { super(name); this.admins = admins; this.lastReport = 'Initial Report'; // private 属性可以在构造函数中初始化 } // protected 方法可以在子类中访问 protected addEmployee(employee: string) { // ... 可以覆盖或增强父类的 protected 方法 super.addEmployee(employee); // 调用父类的 protected 方法 } // private 属性/方法不能在子类中直接访问 // getReport() { // console.log(this.lastReport); // 错误:lastReport 是 private // }
}
const it = new ITDepartment(“IT”, [“Max”]);
// it.addEmployee(“Jane”); // 错误:addEmployee 在父类是 protected,子类继承过来也是 protected (如果子类没有覆盖) 或 public (如果子类覆盖并声明为 public)
// 如果子类覆盖并声明为 public:
class AnotherITDepartment extends Department {
addEmployee(employee: string) { // 声明为 public
super.addEmployee(employee);
console.log(Added ${employee} to IT department.
);
}
}
const anotherIT = new AnotherITDepartment(“AnotherIT”);
anotherIT.addEmployee(“Tom”); // OK
“` -
抽象类与抽象方法
抽象类不能被直接实例化,它主要用于定义派生类的基类。抽象方法只在抽象类中声明,不包含实现,必须在派生类中实现。
“`typescript
abstract class Shape {
constructor(public color: string) { } // 属性可以在构造函数中声明并带有修饰符abstract getArea(): number; // 抽象方法,必须在子类中实现 // 抽象类也可以有具体实现的方法 toString(): string { return `Shape with color ${this.color}`; }
}
class Circle extends Shape {
constructor(color: string, public radius: number) {
super(color);
}// 实现抽象方法 getArea getArea(): number { return Math.PI * this.radius * this.radius; }
}
class Rectangle extends Shape {
constructor(color: string, public width: number, public height: number) {
super(color);
}// 实现抽象方法 getArea getArea(): number { return this.width * this.height; }
}
// const myShape = new Shape(“red”); // 错误:抽象类不能被实例化
const myCircle = new Circle(“blue”, 5);
console.log(myCircle.getArea());
console.log(myCircle.toString());const myRectangle = new Rectangle(“green”, 10, 20);
console.log(myRectangle.getArea());
“`
7. 类型别名(Type Aliases)
type
关键字可以用来给任何类型创建一个新名字,也就是类型别名。类型别名不会创建一个新类型,它只是给现有类型提供了一个名字。
“`typescript
type Age = number;
type Name = string;
type IsStudent = boolean;
let myAge: Age = 30;
let myName: Name = “Alice”;
let studentStatus: IsStudent = true;
// 可以为联合类型创建别名
type StringOrNumber = string | number;
let value: StringOrNumber = “hello”;
value = 123; // OK
// 可以为对象形状创建别名 (与接口类似)
type Point = {
x: number;
y: number;
};
function printPoint(p: Point) {
console.log(x: ${p.x}, y: ${p.y}
);
}
printPoint({ x: 10, y: 20 });
// 可以为函数类型创建别名
type GreetFunc = (name: string) => string;
const greetingFn: GreetFunc = (personName) => Hello, ${personName}!
;
console.log(greetingFn(“Bob”));
“`
类型别名 vs 接口
- 接口(Interface) 主要用于定义对象的形状,类可以实现接口。
- 类型别名(Type) 可以为任何类型创建别名,包括原始类型、联合类型、交叉类型、元组以及对象形状。
在定义对象形状时,接口通常是首选,因为它们可以被扩展 (extends
) 和实现 (implements
),这在面向对象和声明合并(Declaration Merging,多个同名接口会合并)方面有优势。而类型别名更适合用于组合其他类型(如联合类型、交叉类型)或为复杂的类型定义一个简短易懂的名字。
8. 联合类型(Union Types)与交叉类型(Intersection Types)
-
联合类型(Union Types)
使用
|
符号,表示一个值可以是多种类型之一。“`typescript
function printId(id: number | string) {
console.log(“Your ID is: ” + id);// 在使用联合类型的变量时,你需要进行类型缩小(Type Narrowing) // 比如使用 typeof 检查 if (typeof id === "string") { console.log(id.toUpperCase()); // OK,现在编译器知道 id 是 string } else { console.log(id * 2); // OK,现在编译器知道 id 是 number }
}
printId(101);
printId(“abc”);
// printId(true); // 错误
“`类型缩小是通过条件判断(如
typeof
,instanceof
, 属性存在检查等)来确定联合类型变量在某个代码块内的具体类型。 -
交叉类型(Intersection Types)
使用
&
符号,表示将多个类型合并为一个新类型。新类型拥有所有被合并类型的成员。“`typescript
interface Left {
left: number;
}interface Right {
right: number;
}// Combined 类型同时拥有 left 和 right 属性
type Combined = Left & Right;function useCombined(value: Combined) {
console.log(Left: ${value.left}, Right: ${value.right}
);
}useCombined({ left: 1, right: 2 }); // OK
// useCombined({ left: 1 }); // 错误:缺少 right
// useCombined({ right: 2 }); // 错误:缺少 left
“`交叉类型常用于混合现有的类型,创建一个包含所有特性的新类型。
9. 字面量类型(Literal Types)
字面量类型允许你将一个特定的字符串、数字或布尔值作为类型。这通常与联合类型结合使用,来限制变量只能是几个预设的常量值之一。
“`typescript
let x: “hello” = “hello”;
// x = “world”; // 错误:Type ‘”world”‘ is not assignable to type ‘”hello”‘.
// 常用于函数参数,限制可能的输入值
function printText(s: string, alignment: “left” | “right” | “center”) {
// …
console.log(Text: ${s}, Alignment: ${alignment}
);
}
printText(“Hello”, “left”); // OK
// printText(“World”, “top”); // 错误:Type ‘”top”‘ is not assignable to type ‘”left” | “right” | “center”‘.
// 也可以用于数字字面量和布尔字面量
type DiceValue = 1 | 2 | 3 | 4 | 5 | 6;
let roll: DiceValue = 4;
// let invalidRoll: DiceValue = 7; // 错误
function getAnswer(answer: true): “yes” { // 参数只能是 true,返回值只能是 “yes”
return “yes”;
}
getAnswer(true); // OK
// getAnswer(false); // 错误
“`
字面量类型让你的代码表达力更强,并且可以在编译时捕获更多错误。
10. 类型断言(Type Assertions)
有时,你比 TypeScript 编译器更了解一个值的类型。类型断言就是用来告诉编译器“相信我,我知道这个值的类型是什么”。它不会在运行时产生任何影响,仅仅是给编译器提供一个提示。
类型断言有两种语法:
-
使用
as
关键字(JSX 中推荐使用这种方式,因为它避免了与 JSX 语法冲突)typescript
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
console.log(strLength); // 输出 16 -
使用尖括号
<>
语法(在.ts
文件中也可以使用,但在 JSX 中与标签语法冲突)typescript
let anotherValue: any = "this is also a string";
let anotherLength: number = (<string>anotherValue).length;
console.log(anotherLength); // 输出 16这两种语法是等价的。
使用场景:
- 当你明确知道从
any
类型转换到具体类型时。 - 当你从 DOM API 获取元素,并知道它是一个特定类型的元素时(例如,获取一个 canvas 元素并断言它是
HTMLCanvasElement
)。 -
在联合类型中,当你知道当前值是联合类型中的某个特定成员时。
“`typescript
interface Foo { foo: number }
interface Bar { bar: string }type FooOrBar = Foo | Bar;
function process(value: FooOrBar) {
// console.log(value.foo); // 错误:FooOrBar 不一定有 foo 属性// 如果你知道 value 是 Foo console.log((value as Foo).foo); // OK (假设运行时 value 确实是 Foo) console.log((value as Bar).bar); // OK (假设运行时 value 确实是 Bar)
}
// 为了安全,通常结合类型守卫使用类型断言
function processSafe(value: FooOrBar) {
if ((value as Foo).foo !== undefined) { // 这是一个简单的属性检查类型守卫
console.log((value as Foo).foo);
} else {
console.log((value as Bar).bar);
}
// 或者更好的类型守卫 function isFoo(v: any): v is Foo { return (v as Foo).foo !== undefined; }
// if (isFoo(value)) { console.log(value.foo); } else { console.log(value.bar); }
}
“`
注意事项:
类型断言是强大的工具,但需要谨慎使用。它会绕过 TypeScript 的类型检查,如果你的断言是错误的(比如断言一个数字是字符串),那么运行时仍然会出错。类型断言只影响编译时,不影响运行时。
11. 泛型(Generics)
泛型是一种在定义函数、接口或类时不预先指定具体类型,而是在使用时再指定类型的机制。这使得代码可以适用于多种类型,同时仍然保持类型安全性。
-
为什么需要泛型?
考虑一个函数,它返回你传入的任何值(身份函数)。
javascript
function identity(arg) {
return arg;
}在 JavaScript 中,这很容易实现。但在 TypeScript 中,如果你不使用泛型,你可能会这样做:
typescript
function identityAny(arg: any): any {
return arg;
}
let outputAny = identityAny("myString"); // outputAny 的类型是 any,丢失了信息
console.log(outputAny.length); // OK,但运行时如果传入的是数字,这里就会报错使用
any
会丢失传入参数的类型信息。我们想要一个函数,它能接收任何类型,并且返回值的类型与输入参数的类型相同。这就是泛型的用武之地。 -
泛型函数
使用
<T>
(或其他字母,通常用 T 表示 Type)作为类型变量,放在函数名后面。“`typescript
function identity(arg: T): T {
return arg;
}// 方式一:编译器可以根据传入的值自动推断出 T 的类型
let output1 = identity(“myString”); // T 被推断为 string
console.log(output1.length); // OKlet output2 = identity(123); // T 被推断为 number
console.log(output2.toFixed(2)); // OK// 方式二:显式指定 T 的类型
let output3 = identity(“another string”); // 显式指定 T 为 string
let output4 = identity(456); // 显式指定 T 为 number // output3 = identity
(123); // 错误:Type ‘number’ is not assignable to type ‘string’.
“`<T>
中的T
是一个占位符,代表一个类型,在使用函数时才确定具体类型。 -
泛型接口
接口也可以使用泛型。
“`typescript
interface GenericIdentityFn{
(arg: T): T;
}function identity
(arg: T): T {
return arg;
}let myIdentity: GenericIdentityFn
= identity; // 这里的 T 被指定为 number console.log(myIdentity(100)); // OK,输入和输出都是 number
// console.log(myIdentity(“hello”)); // 错误:Argument of type ‘string’ is not assignable to parameter of type ‘number’.
“`常见的内置泛型接口有
Array<T>
,Promise<T>
等。“`typescript
let numList: Array= [1, 2, 3]; // T 是 number
let strList: string[] = [“a”, “b”]; // 这是 Array的简写形式 async function fetchData(): Promise
{
// … fetch data
return “data”;
}let data: Promise
= fetchData(); // Promise 解析后的值是 string
“` -
泛型类
类也可以使用泛型。
“`typescript
class GenericNumber{
zeroValue: T;
add: (x: T, y: T) => T;constructor(zero: T, addFn: (x: T, y: T) => T) { this.zeroValue = zero; this.add = addFn; }
}
let myGenericNumber = new GenericNumber
(0, (x, y) => x + y);
console.log(myGenericNumber.add(5, 10)); // OK, T 是 numberlet stringNumeric = new GenericNumber
(“”, (x, y) => x + y);
console.log(stringNumeric.add(“hello”, “world”)); // OK, T 是 string
“`注意:类本身的静态成员不能使用类声明的泛型类型。
-
泛型约束
有时你希望泛型类型变量具有某个特定接口或类型,这时可以使用泛型约束。
“`typescript
interface Lengthwise {
length: number;
}// T 必须满足 Lengthwise 接口 (即必须有一个 number 类型的 length 属性)
function loggingIdentity(arg: T): T {
console.log(arg.length); // OK,现在知道 arg 有 length 属性
return arg;
}loggingIdentity({ length: 10, value: 3 }); // OK
// loggingIdentity(3); // 错误:Argument of type ‘number’ is not assignable to parameter of type ‘Lengthwise’.
// loggingIdentity(“hello”); // OK,字符串有 length 属性
“`
12. 模块(Modules)
TypeScript 支持使用模块来组织代码。模块可以封装变量、函数、类、接口等,并将它们暴露给其他模块使用。TypeScript 遵从 ECMAScript 2015 的模块语法,使用 import
和 export
关键字。
-
导出 (Export)
在任何声明(变量、函数、类、类型别名、接口等)前加上
export
关键字。“`typescript
// math.ts
export const PI = 3.14159;export function add(x: number, y: number): number {
return x + y;
}export interface Serializable {
serialize(): string;
}export class MyClass implements Serializable {
serialize() {
return “serialized”;
}
}// 默认导出 (每个模块最多一个 default export)
export default class Calculator {
// …
}
“` -
导入 (Import)
使用
import
关键字来导入其他模块导出的成员。“`typescript
// main.ts
import { PI, add, MyClass, Serializable } from “./math”; // 命名导入
import Calculator from “./math”; // 默认导入
// import * as math from “./math”; // 导入整个模块作为一个命名空间console.log(PI);
console.log(add(2, 3));let myObj: Serializable = new MyClass();
console.log(myObj.serialize());let calc = new Calculator();
// …使用 calc
“`在使用模块时,
tsconfig.json
中的module
选项非常重要,它决定了编译器生成哪种模块系统的代码(如commonjs
,esnext
,amd
等)。现代前端开发通常使用esnext
或es2020
等,由 Webpack、Rollup 或 Parcel 等打包工具进行处理。Node.js 环境则常用commonjs
或esnext
(配合.mjs
文件或"type": "module"
配置)。
13. 与 JavaScript 代码的协同
在实际项目中,你可能需要使用大量的 JavaScript 库(如 React, Lodash, jQuery)。TypeScript 如何知道这些库的类型信息呢?答案是声明文件(Declaration Files)。
-
声明文件(
.d.ts
)声明文件以
.d.ts
为扩展名,它们只包含类型声明,没有具体的实现代码。这些文件描述了现有的 JavaScript 库或模块的类型信息,让 TypeScript 编译器能够理解它们。例如,一个简单的
jquery.d.ts
文件可能包含:“`typescript
// 部分 jquery.d.ts
interface JQueryextends Iterable {
// … 许多方法和属性的签名
html(htmlString: string): this;
html(): string;
css(propertyName: string | JQuery.PlainObject): string;
css(propertyName: string, value: string | number): this;
// …
}// 定义全局变量 $ 和 jQuery 的类型
declare const $: JQuery;
declare const jQuery: JQuery;// 定义命名空间
declare namespace JQuery {
interface Event {
// … 事件相关的类型
}
// … 更多定义
}
“`有了这样的声明文件,你在 TypeScript 代码中就可以像使用 TypeScript 库一样使用 jQuery,并获得完整的类型提示和检查。
“`typescript
// using-jquery.ts (需要安装 @types/jquery)
// import $ from ‘jquery’; // 如果库是模块化的,这样导入$(‘#my-div’).html(‘Hello from TypeScript!’); // 类型检查通过
// $(‘#my-div’).html(123); // 错误:Argument of type ‘number’ is not assignable to parameter of type ‘string’.
“` -
使用
@types
库手动编写声明文件是非常繁琐的。幸运的是,社区维护了一个庞大的声明文件仓库:DefinitelyTyped。这个仓库中的声明文件通过
@types
组织在 npm 上。要为一个流行的 JavaScript 库安装其对应的类型声明文件,通常只需运行:
bash
npm install --save-dev @types/libraryname例如,为 jQuery 安装类型声明:
bash
npm install --save-dev @types/jquery为 React 安装类型声明:
bash
npm install --save-dev @types/react @types/react-dom安装后,TypeScript 编译器会自动在
node_modules/@types
目录中找到这些声明文件,从而为对应的 JavaScript 库提供类型信息。
14. 深入 tsconfig.json
tsconfig.json
文件是 TypeScript 项目的核心,它包含了编译器选项和项目文件配置。理解其中的关键选项对于有效地使用 TypeScript 至关重要。
生成 tsconfig.json
文件:tsc --init
“`json
// tsconfig.json (部分常用选项)
{
“compilerOptions”: {
/ 项目选项 /
“rootDir”: “./src”, // TypeScript 源代码的根目录
“outDir”: “./dist”, // 编译输出的 JavaScript 文件的目录
“declaration”: true, // 是否生成 .d.ts 声明文件 (对于库开发很重要)
“sourceMap”: true, // 是否生成 .map 文件 (用于调试)
/* 模块和目标 */
"target": "es2016", // 编译输出的 JavaScript 版本 (如 es5, es2015, esnext)
"module": "commonjs", // 编译输出的模块系统 (如 commonjs, esnext)
// "lib": [], // 包含哪些内置库的声明文件 (如 "es2015", "dom"),默认根据 target 推断
"esModuleInterop": true, // 简化 CommonJS 模块的导入 (allows `import * as foo from "foo"` and `import foo from "foo"` for CommonJS modules)
"allowSyntheticDefaultImports": true, // 允许从没有 default export 的模块进行 default import (结合 esModuleInterop 使用)
/* 严格类型检查选项 */
"strict": true, // **强烈推荐**:启用所有严格类型检查选项,包括以下选项
// "noImplicitAny": true, // 不允许隐式的 any 类型
// "strictNullChecks": true, // 启用严格的 null 和 undefined 检查
// "strictFunctionTypes": true, // 启用严格的函数类型检查
// "strictBindCallApply": true, // 启用严格的 bind, call, apply 方法检查
// "strictPropertyInitialization": true, // 检查类的非 undefined 属性是否在构造函数中初始化
// "noImplicitThis": true, // 检查 this 的隐式 any 类型
// "alwaysStrict": true, // 在编译输出的 JS 文件中启用 "use strict"
/* 其他类型检查选项 */
"noUnusedLocals": true, // 报告未使用的局部变量
"noUnusedParameters": true, // 报告未使用的函数参数
"noImplicitReturns": true, // 报告函数没有明确返回值的分支
"noFallthroughCasesInSwitch": true, // 检查 switch 语句中是否有未 break 的 case
/* JSX */
// "jsx": "react", // 支持 JSX 语法,并指定 JSX 编译方式
/* 路径映射 (Path Mapping) */
// "baseUrl": "./", // 用于解析非相对模块名的基目录
// "paths": {}, // 模块名到路径的映射,用于更方便的模块导入 (如 @/components 映射到 src/components)
},
“include”: [ // 哪些文件或目录需要编译
“src//*.ts”,
“src//*.tsx” // 如果使用 React
],
“exclude”: [ // 哪些文件或目录不需要编译 (优先级高于 include)
“node_modules”,
“dist”
]
}
“`
strict: true
选项:
这是最重要的一个选项。开启 strict: true
会同时启用许多严格的类型检查规则,如 noImplicitAny
, strictNullChecks
等。虽然在迁移现有项目时可能需要逐步开启,但对于新项目,强烈建议一开始就启用 strict: true
,这将为你提供最大的类型安全保障,减少潜在的运行时错误。
15. TypeScript 在现代前端/后端框架中的应用(简述)
TypeScript 已经成为现代 Web 开发的事实标准之一。
-
前端框架:
- Angular: Angular 完全使用 TypeScript 构建,并且官方推荐和鼓励使用 TypeScript 进行应用开发。
- React: 通过安装
@types/react
和@types/react-dom
,以及配置好tsconfig.json
(设置"jsx": "react"
),你就可以在 React 项目中使用 TypeScript 编写组件、Hooks 等,获得优秀的类型提示和错误检查。 - Vue: Vue 3 核心和官方库大量使用了 TypeScript,对 TypeScript 的支持非常友好。你可以在 Vue CLI 或 Vite 创建项目时选择 TypeScript 模板。
-
后端框架 (Node.js):
- NestJS: 一个基于 Node.js 的渐进式框架,完全支持 TypeScript,提供了模块化、依赖注入等企业级特性。
- Express/Koa: 通过安装
@types/express
或@types/koa
,你可以在使用 Express 或 Koa 构建后端服务时享受到 TypeScript 的优势。可以使用ts-node
直接运行.ts
文件进行开发调试,最终编译成 JS 部署。
16. 总结与下一步
通过本文,我们了解了 TypeScript 的起源、核心优势,学习了基础类型、类型注解、接口、类、泛型等重要概念,并初步认识了 tsconfig.json
和如何在项目中集成 TypeScript。
TypeScript 为 JavaScript 世界带来了久违的类型安全和强大的工具支持,它能够帮助你构建更健壮、可读性更好、更易于维护的大型应用。虽然学习曲线相比纯 JavaScript 稍高,但投入的时间成本将在项目的长期开发和维护中获得丰厚的回报。
下一步:
- 实践: 创建一个小型项目,尝试应用本文学到的概念。从简单的功能开始,逐步增加复杂度。
- 阅读官方文档: TypeScript 官方文档(https://www.typescriptlang.org/docs/)是最好的参考资料,详细解释了所有功能。
- 了解更高级特性: TypeScript 还有很多高级特性,如类型守卫、类型推断的细节、条件类型、映射类型、装饰器等,可以在实践中逐步学习。
- 参与社区: 查看 GitHub 上的 TypeScript 项目,学习其他人的代码,或者参与到开源项目中。
TypeScript 是一门持续发展的语言,不断有新的特性加入。保持学习的热情,掌握 TypeScript,将极大地提升你的开发能力和职业竞争力。祝你在 TypeScript 的学习之旅中取得成功!