TypeScript
什么是 TypeScript
TypeScript 是一种由微软开发的开源编程语言,它是 JavaScript 的一个超集。这意味着 TypeScript 包含了 JavaScript 的所有功能,并在此基础上添加了额外的静态类型和其他特性。
TypeScript 和 JavaScript 的区别如下:
类型系统:TypeScript 具有静态类型系统,在编译时就能检查变量的类型。这有助于在开发过程中发现并解决潜在的类型相关错误,提高了代码的可维护性和健壮性。而 JavaScript 是一种动态类型语言,类型在运行时才会确定,因此不会在编译时进行类型检查。
类型注解:在 TypeScript 中,可以通过类型注解为变量、函数参数、函数返回值等添加类型信息,这有助于代码的可读性和理解性。而 JavaScript 并不支持显式的类型注解,类型通常隐式地从赋值中推断出来。
新特性支持:TypeScript 提供了一些 JavaScript 尚未支持的新特性,如异步函数、装饰器、可选参数、泛型等。这些特性使得 TypeScript 更适合于大型项目的开发。
编译:TypeScript 代码需要编译成 JavaScript,才能在浏览器或 Node.js 等 JavaScript 运行环境中执行。而 JavaScript 是一种解释型语言,不需要编译过程,直接在运行时执行。
工具支持:由于 TypeScript 具有静态类型系统,因此可以提供更强大的 IDE 支持,如代码自动补全、类型检查、重构等。JavaScript 的 IDE 支持相对较弱。
类型
在 TypeScript 中,类型用于描述变量、函数参数、函数返回值以及其他数据的结构和行为。类型系统是 TypeScript 最强大的特性之一,它允许开发者在代码中明确地指定数据的类型。
使用类型的主要原因有以下几点:
类型安全性:通过类型,可以在编译时捕获许多常见的错误,例如将错误类型的数据赋值给变量、使用未定义的属性或方法等。这种类型安全性可以帮助开发者在开发阶段尽早发现并修复潜在的错误,提高代码的可靠性和健壮性。
代码提示和文档:类型系统提供了更多的信息,IDE 和编辑器可以根据这些信息提供更准确的代码提示、自动完成和文档查看。这有助于开发者更快地编写代码,并且更容易理解和维护代码。
提高可读性和可维护性:通过类型注解,可以清晰地表达代码的意图,使得代码更易于阅读和理解。此外,类型信息也有助于团队成员之间更好地交流和协作。
重构和调试:类型信息可以帮助 IDE 和编辑器在进行重构操作时更加智能,减少错误。同时,在调试过程中,类型信息也有助于更快地定位和解决问题。
更好的工具支持:类型系统使得 IDE 和编辑器可以提供更强大的功能,如代码检查、错误提示、重构等,从而提高开发效率。
类型推断
在 TypeScript 中,类型推断(Type Inference)是指编译器根据变量的初始化值来推断其类型的过程。TypeScript 的类型推断使得开发者在不显式指定类型的情况下,也能够获得类型安全的优势。
当开发者声明一个变量并为其赋值时,TypeScript 编译器会根据赋值的表达式推断出变量的类型。这种类型推断发生在变量声明时,而不是在变量使用时。例如:
let myVariable = 10; // TypeScript 推断 myVariable 的类型为 number
在这个例子中,变量 myVariable
被初始化为整数 10
,因此 TypeScript 推断出 myVariable
的类型为 number
。
类型推断的好处在于它可以减少代码中的重复,使代码更简洁、更具可读性,并且能够自动适应变量值的类型变化。但是需要注意的是,当变量被初始化为多种类型的值时,TypeScript 会推断出最适合的通用类型。例如:
let myVariable = 10; // TypeScript 推断 myVariable 的类型为 number
myVariable = "Hello"; // 错误,因为 TypeScript 推断的类型为 number,无法赋值为字符串
在上述示例中,尽管后续尝试将变量 myVariable
赋值为字符串,但由于 TypeScript 推断的类型是 number
,因此赋值操作会被 TypeScript 编译器标记为错误。
类型断言
在 TypeScript 中,类型断言(Type Assertion)是指允许开发者手动指定变量的类型,告诉编译器某个值的具体类型。类型断言有两种形式:尖括号语法和 as 语法。
尖括号语法
尖括号语法是最早引入的类型断言语法,在变量名前使用尖括号,将要断言的类型放在尖括号内。例如:
typescriptlet myVariable: any = "Hello, TypeScript!"; // 使用尖括号语法进行类型断言 let strLength: number = (<string>myVariable).length; console.log(strLength); // 输出 17
as 语法
as 语法是后来引入的类型断言语法,通过在变量名后使用 as 关键字来进行类型断言。例如:typescriptlet myVariable: any = "Hello, TypeScript!"; let strLength: number = (myVariable as string).length; // 使用 as 语法进行类型断言 console.log(strLength); // 输出 17
类型断言的作用主要有以下几点:
- 告诉编译器变量的具体类型:当编译器无法推断出变量的类型时,可以通过类型断言告诉编译器变量的具体类型,从而避免编译器错误。
- 在编译阶段绕过类型检查:有时候开发者可能知道某个值的确切类型,但编译器无法确定,这时可以使用类型断言绕过编译器的类型检查。
- 在处理不同类型的值时进行转换:当需要将一个值转换为特定类型时,可以使用类型断言来实现。例如,将一个父类型转换为子类型,或将 any 类型转换为确定的类型。
需要注意的是,类型断言不会在运行时进行任何检查或转换,它仅仅是在编译阶段起作用,告诉编译器如何处理某个值的类型。因此,如果类型断言不正确,可能会导致运行时错误。
类型别名
在 TypeScript 中,类型别名(Type Aliases)是一种用于给现有类型起别名的机制。它允许开发者为复杂的类型或联合类型定义一个新的名称,从而提高代码的可读性和可维护性。
类型别名使用 type
关键字进行定义。下面是一些使用类型别名的示例:
基本类型别名
typescripttype ID = number; type Username = string; let userId: ID = 1001; let username: Username = "user123";
复杂类型别名
typescripttype Point = { x: number; y: number; }; let origin: Point = { x: 0, y: 0 };
函数类型别名:
typescripttype Greeting = (name: string) => string; let greet: Greeting = (name) => `Hello, ${name}!`;
联合类型别名:
typescripttype Result = "success" | "error"; let status: Result = "success";
联合类型和交叉类型
联合类型(Union Types)
联合类型表示一个值可以是多种类型之一。使用|
符号将多种类型连接在一起形成联合类型。例如:typescript// 联合类型示例 let myVariable: string | number; // myVariable 可以是 string 类型或 number 类型 myVariable = "Hello"; // 正确 myVariable = 10; // 正确 // myVariable = true; // 错误,布尔类型不属于联合类型 string | number
在联合类型中,当我们对变量进行操作时,只能访问所有类型的共有成员。例如,如果
myVariable
是string | number
,那么只能访问string
和number
的共有成员。交叉类型(Intersection Types)
交叉类型表示一个值同时具有多种类型的特性。使用&
符号将多种类型连接在一起形成交叉类型。例如:typescript// 交叉类型示例 interface A { propA: number; } interface B { propB: string; } let myVariable: A & B; // myVariable 同时具有 A 和 B 的属性 myVariable = { propA: 10, propB: "Hello" }; // 正确
在交叉类型中,
myVariable
必须同时具有A
和B
的所有属性。
它们的区别在于:
- 联合类型表示一个值可以是多种类型之一,使用
|
符号连接。 - 交叉类型表示一个值同时具有多种类型的特性,使用
&
符号连接。
类型守卫
在 TypeScript 中,类型守卫是一种用于缩小类型范围的技术,它通过一些条件检查来确定一个变量的类型,并在代码块内部使得该变量的类型更为具体。这样做可以在代码中更安全地使用类型,并且在后续的代码中可以更准确地推断类型,以提供更好的代码提示和类型检查。
类型守卫通常通过一些条件语句,比如 typeof
、instanceof
、自定义类型谓词函数等来实现。当 TypeScript 能够确定变量的类型时,它会根据这些条件来缩小变量的类型范围。
以下是一个类型守卫的示例:
interface Bird {
fly(): void;
layEggs(): void;
}
interface Fish {
swim(): void;
layEggs(): void;
}
function getPet(): Bird | Fish {
// 模拟随机返回鸟或鱼
return Math.random() < 0.5 ? { fly: () => console.log("Bird flying"), layEggs: () => console.log("Bird laying eggs") } : { swim: () => console.log("Fish swimming"), layEggs: () => console.log("Fish laying eggs") };
}
let pet = getPet();
if ('fly' in pet) {
// 这里 TypeScript 知道 pet 是 Bird 类型
pet.fly();
} else {
// 这里 TypeScript 知道 pet 是 Fish 类型
pet.swim();
}
在这个例子中,getPet
函数返回一个 Bird
或 Fish
类型的对象,然后我们使用 in
关键字进行类型守卫。当 fly
存在于 pet
对象时,TypeScript 推断 pet
的类型为 Bird
,否则推断为 Fish
。
类型守卫可以让我们在代码中更安全地使用不同类型的变量,并且能够更方便地在不同的代码分支中应用不同类型的操作。
泛型
在 TypeScript 中,泛型(Generics)是一种允许开发者在定义函数、类、接口等可重用组件时,使用参数化类型的方式。泛型能够增强代码的灵活性和重用性,使得我们可以编写更加通用和抽象的代码,同时保持类型安全。
泛型允许开发者在定义函数、类或接口时,不指定具体的类型,而是使用一个占位符来表示参数类型。当使用泛型的函数、类或接口时,可以通过传入实际类型来替换泛型占位符,从而实现对不同类型的支持。
下面是一个简单的使用泛型的例子,定义了一个泛型函数 identity
,该函数接受一个参数并返回相同的参数:
function identity<T>(arg: T): T {
return arg;
}
let output1 = identity<string>("Hello, TypeScript!"); // 显式指定泛型参数
console.log(output1); // 输出 "Hello, TypeScript!"
let output2 = identity(10); // 类型推断,自动推断泛型参数为 number
console.log(output2); // 输出 10
在上述示例中,<T>
表示这是一个泛型函数,并且 T
是一个占位符,可以代表任意类型。在函数体中,arg
参数的类型被指定为 T
,表示参数和返回值的类型是相同的。在调用函数时,可以显式地传入泛型参数(例如 identity<string>("Hello, TypeScript!")
),也可以让 TypeScript 根据传入的参数自动推断泛型参数的类型(例如 identity(10)
)。
接口
在 TypeScript 中,接口(Interface)是一种用于描述对象的形状(Shape)的抽象结构。它定义了对象应该具有的属性和方法,但并不实现具体的行为。接口在 TypeScript 中扮演着定义和约束数据结构的角色。
接口的作用有以下几点:
定义对象的形状:通过接口可以定义对象应该具有的属性和方法,从而明确了对象的结构。这有助于在开发过程中约束对象的类型,提高代码的可读性和可维护性。
typescriptinterface Person { name: string; age: number; } let person: Person = { name: "Alice", age: 30 };
类型检查:接口可以用于对对象进行类型检查,当对象的结构与接口不符时,TypeScript 编译器会给出相应的错误提示,帮助开发者尽早发现潜在的错误。
typescriptinterface Person { name: string; age: number; } let person: Person = { name: "Alice", age: "30" // 错误:类型不匹配,应为 number 类型 };
代码提示和文档:使用接口可以提供更多的类型信息,IDE 和编辑器可以根据接口提供更准确的代码提示和文档信息,从而加快开发速度并提高代码的可维护性。
可选属性和只读属性:接口支持定义可选属性和只读属性,灵活地满足不同场景下的需求。
typescriptinterface Config { readonly apiUrl: string; // 只读属性,不可修改 apiKey?: string; // 可选属性,可以存在也可以不存在 } let config: Config = { apiUrl: "https://example.com/api", // apiKey: "123456" // 可选属性可以省略 };
枚举
在 TypeScript 中,枚举(Enum)是一种用于命名一组命名常量的数据类型,它允许为一组相关的值分配有意义的名称。枚举提供了一种更具可读性和可维护性的方式来表示一组有限的值。
使用枚举可以定义一组相关的常量,这些常量具有一个共同的类型。每个枚举成员都具有一个关联的数字值,默认情况下是从 0 开始自动递增。但也可以手动指定每个枚举成员的值。
以下是一个示例,展示了如何定义和使用枚举:
// 定义一个名为 Color 的枚举类型,包含 Red、Green、Blue 三种颜色
enum Color {
Red,
Green,
Blue
}
// 使用枚举类型 Color 声明一个变量
let color: Color = Color.Green;
// 访问枚举成员的值
console.log(color); // 输出 1,因为 Green 的值是 1(从 0 开始计数)
// 可以根据枚举值获取枚举成员的名称
console.log(Color[1]); // 输出 "Green"
此外,我们还可以手动指定枚举成员的值:
// 手动指定枚举成员的值
enum Color {
Red = 1,
Green = 2,
Blue = 4
}
let color: Color = Color.Green;
console.log(color); // 输出 2
模块(module)
在 TypeScript 中,模块(Module)是一种用于组织和封装代码的机制,它允许将相关的代码封装在一个独立的单元中,以便于管理和复用。模块可以包含变量、函数、类等,并且可以通过导出(export)和导入(import)来暴露和引用模块中的成员。
模块有两种主要的类型:内部模块(也称为命名空间)和外部模块(也称为模块)。
内部模块(命名空间 namespace)
内部模块使用
namespace
关键字来定义,它可以将相关的代码封装在一个命名空间中,避免命名冲突,并提供更好的组织结构。下面是一个内部模块的示例:typescriptnamespace Geometry { export interface Point { x: number; y: number; } export function distance(p1: Point, p2: Point): number { const dx = p2.x - p1.x; const dy = p2.y - p1.y; return Math.sqrt(dx * dx + dy * dy); } } let point1: Geometry.Point = { x: 0, y: 0 }; let point2: Geometry.Point = { x: 3, y: 4 }; let distance = Geometry.distance(point1, point2); console.log(distance); // 输出 5
外部模块(模块)
外部模块使用
module
或export
关键字来定义,它可以将相关的代码封装在一个独立的文件中,并通过export
关键字来暴露模块中的成员。其他文件可以通过import
关键字来引入模块中的成员。下面是一个外部模块的示例:typescript// point.ts export interface Point { x: number; y: number; } // distance.ts import { Point } from "./point"; export function distance(p1: Point, p2: Point): number { const dx = p2.x - p1.x; const dy = p2.y - p1.y; return Math.sqrt(dx * dx + dy * dy); } // main.ts import { Point } from "./point"; import { distance } from "./distance"; let point1: Point = { x: 0, y: 0 }; let point2: Point = { x: 3, y: 4 }; let dist = distance(point1, point2); console.log(dist); // 输出 5
通过使用模块,可以将代码分割成更小的单元,并通过导入和导出机制来组织和管理代码,提高代码的可维护性和复用性。
访问修饰符
在 TypeScript 中,有三种主要的访问修饰符用于类的成员(属性和方法):public
、private
和 protected
。
public
- 默认的访问修饰符,如果成员没有使用其他访问修饰符,则默认为
public
。 - 公共成员可以在任何地方访问(类内部、子类和类的实例)。
- 默认的访问修饰符,如果成员没有使用其他访问修饰符,则默认为
private
- 私有成员只能在声明它们的类内部访问,外部无法访问。
- 该成员对于类的实例和子类都是不可见的。
protected
- 受保护成员可以在声明它们的类内部和子类中访问,但是对于类的实例是不可见的。
protected
成员在类的外部是不可访问的。
示例:
class MyClass {
public publicVar: number; // 公共成员,默认访问修饰符
private privateVar: string; // 私有成员
protected protectedVar: boolean; // 受保护成员
constructor(publicVar: number, privateVar: string, protectedVar: boolean) {
this.publicVar = publicVar;
this.privateVar = privateVar;
this.protectedVar = protectedVar;
}
}
let obj = new MyClass(10, "private", true);
console.log(obj.publicVar); // 可以访问
// console.log(obj.privateVar); // 编译错误,私有成员不能在类的实例外部访问
// console.log(obj.protectedVar); // 编译错误,受保护成员不能在类的实例外部访问
访问修饰符可以帮助你控制类的成员在外部的可见性和访问权限,增加了代码的安全性和可维护性。
模块解析策略
在 TypeScript 中,模块解析策略指的是编译器用来查找和加载模块文件的方式。当你在 TypeScript 中使用 import
或 export
语句时,编译器需要找到相应的模块文件,模块解析策略定义了编译器应该如何解析这些模块文件的依赖关系。
以下是 TypeScript 中常用的模块解析策略选项:
Classic
- 也称为 Node.js 解析策略。
- 用于解析 CommonJS 和 AMD 模块。
- 当使用 Node.js 风格的模块解析时,TypeScript 将尝试从当前文件所在的目录开始查找模块。
Node
- 与
Classic
解析策略类似,但会优先从 Node.js 的模块解析算法开始查找。 - 在 Node.js 环境下推荐使用此解析策略。
- 与
Module Resolution
- 这是 TypeScript 用于解析 ECMAScript 模块的默认解析策略。
- 当使用 ES 模块语法(
import
和export
)时,默认会使用这个策略。 - 它可以解析相对路径和绝对路径的模块。
你可以在 tsconfig.json
文件中的 moduleResolution
字段中指定你希望使用的解析策略,例如:
{
"compilerOptions": {
"moduleResolution": "node"
}
}
通过选择合适的模块解析策略,可以确保 TypeScript 编译器能够正确地查找和加载模块文件,并使你的项目具有更好的可维护性和灵活性。
装饰器
在 TypeScript 中,装饰器是一种特殊的语法,用于在类、方法、访问器、属性或参数声明之前进行修饰和扩展。装饰器是一种元编程(metaprogramming)的技术,允许你在不修改源代码的情况下,动态地修改类或类成员的行为。
装饰器通过在被修饰的声明之前使用 @
符号和一个装饰器工厂函数来使用。装饰器可以接受参数,并且根据参数的不同可以有不同的行为。
装饰器的作用包括但不限于:
添加元数据(Metadata): 装饰器可以用来为类或类成员添加元数据,这些元数据可以在运行时被检索和利用,比如用于路由控制、验证等。
修改行为: 装饰器可以用来修改类或类成员的行为,例如在方法调用前后执行特定逻辑、检查输入参数的有效性、实现缓存等。
代码重用和组合: 装饰器可以用来封装通用的行为,使得这些行为可以在多个类或类成员中重复使用,从而提高了代码的可维护性和可重用性。
与框架集成: 装饰器在许多 TypeScript 框架中被广泛使用,例如 Angular 和 NestJS,用于实现依赖注入、路由注册、中间件等功能。
代码示例:
// 装饰器工厂函数,用于创建装饰器
function Log(target: any, key: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
// 重写原始方法
descriptor.value = function(...args: any[]) {
console.log(`Calling ${key} with arguments: ${JSON.stringify(args)}`);
const result = originalMethod.apply(this, args);
console.log(`Method ${key} returned: ${JSON.stringify(result)}`);
return result;
};
return descriptor;
}
class Example {
@Log
add(x: number, y: number): number {
return x + y;
}
}
const example = new Example();
const sum = example.add(3, 5); // 输出:Calling add with arguments: [3,5] Method add returned: 8
console.log(`Sum: ${sum}`); // 输出:Sum: 8
Log
装饰器是一个装饰器工厂函数,它接受三个参数:target
是被修饰的类的原型对象,key
是被修饰成员的键(方法名),descriptor
是该成员的属性描述符。- 在装饰器工厂函数内部,我们获取了原始方法,并重写了它,以在调用方法之前和之后输出日志。
@Log
装饰器被应用在add
方法上,这使得每次调用add
方法时都会自动输出日志。- 在实例化
Example
类并调用add
方法时,你会看到在控制台输出了相应的日志信息。