TypeScript笔记 | 青训营
TypeScript 简介
TypeScript 是一种开源的编程语言,它是 JavaScript 的一个超集。它添加了静态类型系统和一些其他功能,使得在开发大型应用时更加可靠和易于维护。TypeScript 最终会被编译成普通的 JavaScript 代码,因此可以在任何支持 JavaScript 的地方运行。
主要特性
-
静态类型系统: TypeScript 强调变量类型的声明和类型检查,这使得在开发过程中能够捕获一些常见的错误,减少运行时错误。
-
类型注解: 在 TypeScript 中,你可以为变量、函数参数和返回值等添加类型注解,明确表明其所期望的数据类型。
-
类和接口: TypeScript 支持面向对象编程,可以定义类和接口,从而提供更丰富的类型描述和代码结构。
-
泛型: TypeScript 允许你创建可重用的组件,适用于多种数据类型。通过泛型,你可以编写更通用和类型安全的代码。
-
枚举: TypeScript 引入了枚举类型,让你可以定义一组命名的常量值,提高代码的可读性。
-
命名空间和模块: TypeScript 支持命名空间和模块化开发,使得代码的组织和分离变得更加简单。
-
装饰器: 装饰器是一种特殊的声明,可以附加到类、方法、属性等上,用于自定义它们的行为。在框架和库的开发中特别有用。
安装和使用
-
安装 TypeScript: 使用 npm 或 yarn 安装 TypeScript 编译器。
npm install -g typescript
-
创建 TypeScript 文件: 创建一个
.ts
后缀的文件,开始编写 TypeScript 代码。 -
编写代码: 使用 TypeScript 的语法和特性编写代码,可以使用类型注解来指定变量的类型。
-
编译代码: 使用 TypeScript 编译器将 TypeScript 代码转换为 JavaScript 代码。
tsc your-file.ts
示例代码
下面是一个简单的 TypeScript 示例,展示了类型注解和类的使用:
// 类型注解
let greeting: string = "Hello, TypeScript!";
// 类的定义
class Person {
private name: string;
constructor(name: string) {
this.name = name;
}
sayHello() {
console.log(`Hello, my name is ${this.name}.`);
}
}
// 创建实例
const person = new Person("Alice");
person.sayHello();
静态类型系统是一种编程语言特性,用于在代码编写阶段检查变量、函数和表达式的数据类型,以捕获潜在的类型错误,提供更强大的类型安全性。与动态类型系统不同,静态类型系统在编译时进行类型检查,而不是在运行时。
以下是静态类型系统的一些关键概念和优势:
静态类型系统
类型声明
在静态类型系统中,你需要为变量、函数参数、函数返回值等明确地指定所期望的数据类型。这通常通过类型注解或类型声明来完成。
// 使用类型注解声明变量的类型
let age: number = 25;
// 函数参数和返回值的类型声明
function add(a: number, b: number): number {
return a + b;
}
类型检查
静态类型系统会在编译时检查代码,确保变量和函数的使用与其声明的数据类型相符合。如果存在不匹配的类型,编译器会报告错误。
let name: string = "Alice";
// 下面这行代码会引发类型错误,因为数字不能赋值给字符串类型的变量
name = 42; // Error: Type 'number' is not assignable to type 'string'.
类型推断
静态类型系统还支持类型推断,即在某些情况下,编译器可以自动推断变量的类型,无需显式注解。
let age = 25; // TypeScript 推断 age 的类型为 number
优势
-
类型安全性: 静态类型系统能够在编译时捕获一些常见的类型错误,避免在运行时出现难以追踪的错误。
-
可读性和维护性: 类型注解使代码更易读懂,因为它们提供了关于变量和函数意图的明确信息。在大型项目中,静态类型系统还能够帮助团队成员理解和修改代码。
-
自文档化: 类型注解可以充当文档,帮助开发者了解代码的预期用法和输入输出。
-
智能编码助手: 静态类型系统能够让代码编辑器提供更准确的代码补全、错误提示和重构建议。
动态类型 vs. 静态类型
动态类型语言(如 JavaScript)在运行时进行类型检查,因此具有更大的灵活性,但在某些情况下可能导致类型相关的错误只在运行时才能发现。
静态类型语言(如 TypeScript、Java、C++)则在编译时执行类型检查,提供了更高的类型安全性,但需要在代码中显式声明和管理类型。
选择使用哪种类型系统取决于项目的需求、团队的偏好和开发的目标。
类型注解
类型注解是一种在代码中明确指定变量、函数参数、函数返回值等的数据类型的方式。在静态类型系统中,类型注解能够帮助编译器进行类型检查,以确保代码中的数据类型使用是正确的。
在 TypeScript 中,使用冒号(:
)后跟一个类型名称来添加类型注解。下面是一些关于类型注解的例子和解释:
变量的类型注解
let age: number; // 声明一个名为 "age" 的变量,它的类型被注解为 number
let name: string; // 声明一个名为 "name" 的变量,它的类型被注解为 string
age = 25; // 合法,age 被赋值为一个 number
name = "Alice"; // 合法,name 被赋值为一个 string
age = "twenty-five"; // 错误,无法将字符串赋值给一个被注解为 number 的变量
函数的类型注解
function add(a: number, b: number): number {
return a + b;
}
let result = add(10, 20); // result 被推断为 number,因为 add 函数的返回类型被注解为 number
函数参数的类型注解
function greet(name: string): void {
console.log(`Hello, ${name}!`);
}
greet("Alice"); // 合法,传入一个 string 类型的参数
greet(42); // 错误,无法将数字赋值给一个要求 string 类型的参数
类属性和方法的类型注解
class Person {
name: string; // 类属性的类型注解
constructor(name: string) {
this.name = name;
}
sayHello(): void {
console.log(`Hello, my name is ${this.name}.`);
}
}
类型注解不仅可以在变量和函数中使用,还可以用于类的属性和方法。它们提供了代码中数据类型的明确表示,有助于编译器检查类型相关的错误,提高了代码的可读性和可维护性。需要注意的是,类型注解仅在编译时有意义,运行时并不会影响代码的行为。
类和接口
在 TypeScript 中,类(Class)和接口(Interface)是两个重要的概念,用于构建面向对象的代码和实现抽象数据类型。它们分别用于创建对象和定义对象的外部形状。让我们深入了解一下类和接口的概念以及如何在 TypeScript 中使用它们。
类(Class)
类是一种将数据和行为组合在一起的结构,用于创建具有相似特征和功能的对象。它是面向对象编程的核心概念之一。
定义类:
class Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
sayHello() {
console.log(`Hello, my name is ${this.name} and I'm ${this.age} years old.`);
}
}
创建实例:
const person = new Person("Alice", 25);
person.sayHello(); // 输出:Hello, my name is Alice and I'm 25 years old.
接口(Interface)
接口是一种描述对象的外部形状的方式,它定义了对象应该具有的属性和方法。接口在 TypeScript 中用于声明一组规范,而不是实现。
定义接口:
interface Shape {
area(): number;
}
class Circle implements Shape {
radius: number;
constructor(radius: number) {
this.radius = radius;
}
area() {
return Math.PI * this.radius * this.radius;
}
}
使用接口:
const circle = new Circle(5);
console.log(circle.area()); // 输出:78.53981633974483
类与接口的比较
-
类: 类用于创建对象,包含属性和方法的定义,可以进行实例化。类可以有构造函数用于初始化对象。
-
接口: 接口用于定义对象的形状,不包含实现,只有属性和方法的声明。接口不能直接被实例化,但可以用于类型检查和约束。
-
继承与实现: 类可以继承其他类,而接口可以被类实现。一个类只能继承一个类,但可以实现多个接口。
-
抽象性: 类可以包含具体的实现,而接口只能包含抽象的声明。
-
相似性: 在某些情况下,类和接口可以达到相似的目的,但它们的用途不同。类用于创建对象的实例,而接口用于定义对象的形状。
总之,类和接口在 TypeScript 中有不同的用途,类用于实现对象的行为和状态,而接口用于定义对象的外部结构。根据具体的需求,你可以选择使用类、接口或它们的组合来构建你的程序。
泛型
泛型(Generics)是一种在编程中用于创建可重用、通用代码的技术。它允许你编写函数、类或接口,可以适用于多种数据类型,而不仅仅限于单一类型。泛型在提高代码的灵活性、可重用性和类型安全性方面发挥着重要作用。
泛型的核心思想是参数化类型,它使得你能够在使用代码时,为特定的数据类型提供具体的类型。
泛型函数示例:
function identity<T>(arg: T): T {
return arg;
}
let output = identity<string>("Hello, TypeScript!"); // 指定泛型类型为 string
let output2 = identity<number>(42); // 指定泛型类型为 number
// TypeScript 类型推断也可以自动推断泛型类型
let inferredOutput = identity(true); // 推断出泛型类型为 boolean
泛型类示例:
class Box<T> {
value: T;
constructor(value: T) {
this.value = value;
}
}
let numberBox = new Box<number>(42);
let stringBox = new Box<string>("Hello");
泛型接口示例:
interface KeyValuePair<K, V> {
key: K;
value: V;
}
let entry: KeyValuePair<string, number> = { key: "age", value: 25 };
泛型不仅可以用于数据类型,还可以用于函数签名、类方法、类属性等地方。泛型的使用可以提高代码的灵活性和可维护性,同时保持类型安全。
泛型约束
有时候你想要对泛型参数的类型进行约束,确保它满足特定的条件。这时可以使用泛型约束。
interface Lengthwise {
length: number;
}
function logLength<T extends Lengthwise>(arg: T): void {
console.log(arg.length);
}
logLength("Hello"); // 输出:5
logLength([1, 2, 3]); // 输出:3
logLength(42); // 错误,因为 number 类型没有 length 属性
在这个例子中,T extends Lengthwise
表示泛型参数 T
必须满足 Lengthwise
接口的约束,即具有 length
属性。
泛型在编写可复用且灵活的代码时非常有用,可以根据不同的数据类型自动适应,提高代码效率和可读性。
枚举
枚举(Enum)是一种用于定义一组命名常量的方式,它可以在代码中更直观地表示一组相关的取值。在 TypeScript 中,枚举提供了一种方便的方式来定义命名的整数常量,从而提高代码的可读性和可维护性。
定义枚举:
enum Direction {
Up,
Down,
Left,
Right,
}
在这个例子中,Direction
枚举定义了四个常量:Up
、Down
、Left
和 Right
。默认情况下,枚举成员的值从 0 开始自增。你也可以显式指定枚举成员的值:
enum Direction {
Up = 1,
Down,
Left,
Right,
}
在这个例子中,Up
的值为 1,Down
为 2,以此类推。
使用枚举:
let playerDirection = Direction.Right;
console.log(playerDirection); // 输出:3
if (playerDirection === Direction.Right) {
console.log("Player is moving to the right.");
}
你可以将枚举成员赋值给变量,然后在代码中使用这些变量。在上面的例子中,playerDirection
被赋值为 Direction.Right
,然后使用了一个条件语句检查它是否等于 Direction.Right
。
字符串枚举:
默认情况下,枚举成员的值是数字。但你也可以使用字符串枚举,其中每个成员的值都是字符串。
enum LogLevel {
Error = "ERROR",
Warning = "WARNING",
Info = "INFO",
}
let logLevel = LogLevel.Warning;
console.log(logLevel); // 输出:WARNING
反向映射:
枚举还提供了一种从值到枚举成员的反向映射。
enum Direction {
Up,
Down,
Left,
Right,
}
let directionName = Direction[2];
console.log(directionName); // 输出:Left
在这个例子中,Direction[2]
返回的是字符串 "Left"
,这是因为枚举成员 Left
的值为 2。
枚举在代码中可以增加可读性,避免使用魔法数值,使代码更具有可维护性。无论是用于表示状态、选项还是一组相关的值,枚举都能起到很好的作用。
命名空间和模板
当你的代码项目变得越来越复杂,为了避免命名冲突和更好地组织代码,你可以使用 TypeScript 中的命名空间(Namespace)和模块(Module)。
命名空间(Namespace)
命名空间是一种将相关的代码组织在一起的方式,以避免全局命名冲突。它可以将相关的函数、类、接口等封装在一个命名空间内部,然后在其他地方通过命名空间进行访问。
定义命名空间:
namespace MyNamespace {
export interface Person {
name: string;
age: number;
}
export function sayHello(person: Person) {
console.log(`Hello, my name is ${person.name} and I'm ${person.age} years old.`);
}
}
使用命名空间:
let person: MyNamespace.Person = { name: "Alice", age: 25 };
MyNamespace.sayHello(person);
在这个例子中,我们创建了一个名为 MyNamespace
的命名空间,里面包含了一个 Person
接口和一个 sayHello
函数。通过 export
关键字,我们将这些成员暴露出来,从而能够在外部使用。
模块(Module)
模块是将代码组织为可重用、可维护的单元。与命名空间不同,模块将代码封装起来,并且默认处于严格模式,不会自动暴露成员到全局作用域。
定义模块:
// math.ts
export function add(a: number, b: number): number {
return a + b;
}
export function subtract(a: number, b: number): number {
return a - b;
}
使用模块:
// app.ts
import { add, subtract } from './math';
console.log(add(10, 5)); // 输出:15
console.log(subtract(10, 5)); // 输出:5
在这个例子中,我们在 math.ts
文件中定义了两个函数,并使用 export
关键字将它们导出。然后在 app.ts
文件中使用 import
关键字导入并使用这些函数。
模块提供了更强大的封装和代码复用机制,适用于构建复杂的应用程序。与命名空间相比,模块更适合用于分布式开发、跨项目共享和管理依赖。
装饰器
装饰器(Decorators)是 TypeScript 的一项特性,用于在类、方法、属性等声明之前进行修饰和增强。装饰器提供了一种便捷的方式来修改类或其成员的行为,同时也能使代码更加易读和模块化。
装饰器在实际开发中常用于 AOP(面向切面编程),用来添加额外的功能或修改现有的行为,例如日志记录、验证、性能分析等。
使用装饰器
在 TypeScript 中,装饰器由 @
符号紧跟一个函数名来表示,并放置在要修饰的目标前面。
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log(`Calling method ${propertyKey} with arguments ${args}`);
const result = originalMethod.apply(this, args);
console.log(`Method ${propertyKey} returned: ${result}`);
return result;
};
return descriptor;
}
class Calculator {
@log
add(a: number, b: number): number {
return a + b;
}
}
const calculator = new Calculator();
calculator.add(10, 20); // 调用 add 方法,并在控制台中输出日志
在这个例子中,@log
装饰器被应用到 add
方法上。装饰器修改了 add
方法的行为,添加了额外的日志输出。装饰器接收三个参数:目标对象、成员名、成员的属性描述符。通过修改属性描述符的 value
,我们可以修改原始方法的实现。
装饰器工厂
装饰器可以有参数,当一个装饰器需要定制化的行为时,可以使用装饰器工厂。
function logWithMessage(message: string) {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log(message);
const result = originalMethod.apply(this, args);
return result;
};
return descriptor;
};
}
class Calculator {
@logWithMessage("Calling add method")
add(a: number, b: number): number {
return a + b;
}
}
在这个例子中,@logWithMessage("Calling add method")
是一个装饰器工厂,它返回一个装饰器函数,用于修改 add
方法的行为。
装饰器为 TypeScript 添加了更大的灵活性和可扩展性,使得在代码中添加、修改功能变得更加优雅和模块化。需要注意的是,装饰器在不同的上下文中(例如类、属性、方法等)可能有不同的行为和作用。