每个开发人员都应该知道的 10 个高级 TypeScript 概念
TypeScript 是一种现代编程语言,由于其增强的类型安全性,经常比 JavaScript 更受欢迎。在本文中,我将分享 10 个最重要的 TypeScript 概念,它们将帮助你提升 TypeScript 编程技能。你准备好了吗?我们开始吧。
1.泛型:使用泛型,我们可以创建可重用的类型,这将有助于处理当前和未来的数据。
泛型示例:
我们可能希望 TypeScript 中的函数接受某种类型的参数,并且返回相同的类型。
function func<T>(args:T):T{
return args;
}
2.具有类型约束的泛型:现在让我们通过定义类型 T 来限制它只接受字符串和整数:
function func<T extends string | number>(value: T): T {
return value;
}
const stringValue = func("Hello"); // Works, T is string
const numberValue = func(42); // Works, T is number
// const booleanValue = func(true); // Error: Type 'boolean' is not assignable to type 'string | number'
3. 泛型接口:
当你需要为对象、类或函数定义适用于多种类型的契约(结构)时,接口泛型非常有用。它们允许你定义一个能够适应不同数据类型的蓝图,同时保持结构的一致性。
// Generic interface with type parameters T and U
interface Repository<T, U> {
items: T[]; // Array of items of type T
add(item: T): void; // Function to add an item of type T
getById(id: U): T | undefined; // Function to get an item by ID of type U
}
// Implementing the Repository interface for a User entity
interface User {
id: number;
name: string;
}
class UserRepository implements Repository<User, number> {
items: User[] = [];
add(item: User): void {
this.items.push(item);
}
getById(idOrName: number | string): User | undefined {
if (typeof idOrName === 'string') {
// Search by name if idOrName is a string
console.log('Searching by name:', idOrName);
return this.items.find(user => user.name === idOrName);
} else if (typeof idOrName === 'number') {
// Search by id if idOrName is a number
console.log('Searching by id:', idOrName);
return this.items.find(user => user.id === idOrName);
}
return undefined; // Return undefined if no match found
}
}
// Usage
const userRepo = new UserRepository();
userRepo.add({ id: 1, name: "Alice" });
userRepo.add({ id: 2, name: "Bob" });
const user1 = userRepo.getById(1);
const user2 = userRepo.getById("Bob");
console.log(user1); // Output: { id: 1, name: "Alice" }
console.log(user2); // Output: { id: 2, name: "Bob" }
4.泛型类:当你希望类中的所有属性都遵循泛型参数指定的类型时,可以使用泛型类。这提供了灵活性,同时确保类的每个属性都与传递给类的类型匹配。
interface User {
id: number;
name: string;
age: number;
}
class UserDetails<T extends User> {
id: T['id'];
name: T['name'];
age: T['age'];
constructor(user: T) {
this.id = user.id;
this.name = user.name;
this.age = user.age;
}
// Method to get user details
getUserDetails(): string {
return `User: ${this.name}, ID: ${this.id}, Age: ${this.age}`;
}
// Method to update user name
updateName(newName: string): void {
this.name = newName;
}
// Method to update user age
updateAge(newAge: number): void {
this.age = newAge;
}
}
// Using the UserDetails class with a User type
const user: User = { id: 1, name: "Alice", age: 30 };
const userDetails = new UserDetails(user);
console.log(userDetails.getUserDetails()); // Output: "User: Alice, ID: 1, Age: 30"
// Updating user details
userDetails.updateName("Bob");
userDetails.updateAge(35);
console.log(userDetails.getUserDetails()); // Output: "User: Bob, ID: 1, Age: 35"
console.log(new UserDetails("30")); // Error: "This will throw error"
5.将类型参数限制为传递类型:有时,我们希望参数类型依赖于其他传递的参数。听起来很令人困惑,让我们看下面的例子。
function getProperty<Type>(obj: Type, key: keyof Type) {
return obj[key];
}
let x = { a: 1, b: 2, c: 3 };
getProperty(x, "a"); // Valid
getProperty(x, "d"); // Error: Argument of type '"d"' is not assignable to parameter of type '"a" | "b" | "c"'.
6.条件类型:通常,我们希望类型要么是某种类型,要么是其他类型。在这种情况下,我们会使用条件类型。
一个简单的例子是:
function func(param:number|boolean){
return param;
}
console.log(func(2)) //Output: 2 will be printed
console.log(func("True")) //Error: string cannot be passed as argument here
稍微复杂一点的例子:
type HasProperty<T, K extends keyof T> = K extends "age" ? "Has Age" : "Has Name";
interface User {
name: string;
age: number;
}
let test1: HasProperty<User, "age">; // "Has Age"
let test2: HasProperty<User, "name">; // "Has Name"
let test3: HasProperty<User, "email">; // Error: Type '"email"' is not assignable to parameter of type '"age" | "name"'.
6. 交叉类型:当我们想要将多个类型合并为一个类型时,这些类型非常有用,允许特定类型从其他各种类型继承属性和行为。
让我们看一个有趣的例子:
// Defining the types for each area of well-being
interface MentalWellness {
mindfulnessPractice: boolean;
stressLevel: number; // Scale of 1 to 10
}
interface PhysicalWellness {
exerciseFrequency: string; // e.g., "daily", "weekly"
sleepDuration: number; // in hours
}
interface Productivity {
tasksCompleted: number;
focusLevel: number; // Scale of 1 to 10
}
// Combining all three areas into a single type using intersection types
type HealthyBody = MentalWellness & PhysicalWellness & Productivity;
// Example of a person with a balanced healthy body
const person: HealthyBody = {
mindfulnessPractice: true,
stressLevel: 4,
exerciseFrequency: "daily",
sleepDuration: 7,
tasksCompleted: 15,
focusLevel: 8
};
// Displaying the information
console.log(person);
7.infer 关键字:当我们想要有条件地确定特定类型时,该infer
关键字很有用,当条件满足时,它允许我们从该类型中提取子类型。
以下是一般语法:
type ConditionalType<T> = T extends SomeType ? InferredType : OtherType;
例如:
type ReturnTypeOfPromise<T> = T extends Promise<infer U> ? U : number;
type Result = ReturnTypeOfPromise<Promise<string>>; // Result is 'string'
type ErrorResult = ReturnTypeOfPromise<number>; // ErrorResult is 'never'
const result: Result = "Hello";
console.log(typeof result); // Output: 'string'
8. 类型变异:这个概念讨论了子类型和父类型之间的关系。
它们有两种类型:
协变:在需要父类型的地方可以使用子类型。
我们来看一个例子:
class Vehicle {
start() {
console.log("Vehicle is running");
}
}
class Car extends Vehicle {
honk() {
console.log("this vehicle honks");
}
}
function vehiclefunc(vehicle: Vehicle) {
vehicle.start();
}
function carfunc(car: Car) {
car.start(); // Works because Car extends Vehicle(inheritance)
car.honk(); // Works because 'Car' has 'honk'
}
let car: Car = new Car();
vehiclefunc(car); // Allowed due to covariance
在上面的例子中,Car 继承了 Vehicle 类的属性,因此将其分配给预期为超类型的子类型是绝对有效的,因为子类型将具有超类型的所有属性。
逆变:这与协变相反。我们在预期子类型存在的地方使用超类型。
class Vehicle {
startEngine() {
console.log("Vehicle engine starts");
}
}
class Car extends Vehicle {
honk() {
console.log("Car honks");
}
}
function processVehicle(vehicle: Vehicle) {
vehicle.startEngine(); // This works
// vehicle.honk(); // Error: 'honk' does not exist on type 'Vehicle'
}
function processCar(car: Car) {
car.startEngine(); // Works because Car extends Vehicle
car.honk(); // Works because 'Car' has 'honk'
}
let car: Car = new Car();
processVehicle(car); // This works because of contravariance (Car can be used as Vehicle)
processCar(car); // This works as well because car is of type Car
// Contravariance failure if you expect specific subtype behavior in the method
当使用逆变时,我们需要小心不要访问特定于子类型的属性或方法,因为这可能会导致错误。
9. 反射:这个概念涉及在运行时确定变量的类型。虽然 TypeScript 主要专注于编译时的类型检查,但我们仍然可以利用 TypeScript 运算符在运行时检查类型。typeof
运算符:我们可以利用typeof
运算符在运行时查找变量的类型。
const num = 23;
console.log(typeof num); // "number"
const flag = true;
console.log(typeof flag); // "boolean"
instanceof
运算符: instanceof 运算符可用于检查对象是否是类或特定类型的实例。
class Vehicle {
model: string;
constructor(model: string) {
this.model = model;
}
}
const benz = new Vehicle("Mercedes-Benz");
console.log(benz instanceof Vehicle); // true
我们可以使用第三方库在运行时确定类型。
10.依赖注入:依赖注入是一种模式,它允许你将代码注入到组件中,而无需实际创建或管理代码。虽然它看起来像是使用库,但实际上有所不同,因为你无需通过 CDN 或 API 安装或导入它。
乍一看,这似乎与使用函数实现可复用性类似,因为两者都允许代码复用。但是,如果我们在组件中直接使用函数,则可能导致它们之间紧密耦合。这意味着函数或其逻辑的任何更改都可能影响到使用它的每个地方。
依赖注入通过将依赖项的创建与使用它们的组件分离来解决了这个问题,使得代码更易于维护和测试。
没有依赖注入的示例
// Health-related service classes without interfaces
class MentalWellness {
getMentalWellnessAdvice(): string {
return "Take time to meditate and relax your mind.";
}
}
class PhysicalWellness {
getPhysicalWellnessAdvice(): string {
return "Make sure to exercise daily for at least 30 minutes.";
}
}
// HealthAdvice class directly creating instances of the services
class HealthAdvice {
private mentalWellnessService: MentalWellness;
private physicalWellnessService: PhysicalWellness;
// Directly creating instances inside the class constructor
constructor() {
this.mentalWellnessService = new MentalWellness();
this.physicalWellnessService = new PhysicalWellness();
}
// Method to get both mental and physical wellness advice
getHealthAdvice(): string {
return `${this.mentalWellnessService.getMentalWellnessAdvice()} Also, ${this.physicalWellnessService.getPhysicalWellnessAdvice()}`;
}
}
// Creating an instance of HealthAdvice, which itself creates instances of the services
const healthAdvice = new HealthAdvice();
console.log(healthAdvice.getHealthAdvice());
// Output: "Take time to meditate and relax your mind. Also, Make sure to exercise daily for at least 30 minutes."
依赖注入示例
// Health-related service interfaces with "I" prefix
interface IMentalWellnessService {
getMentalWellnessAdvice(): string;
}
interface IPhysicalWellnessService {
getPhysicalWellnessAdvice(): string;
}
// Implementations of the services
class MentalWellness implements IMentalWellnessService {
getMentalWellnessAdvice(): string {
return "Take time to meditate and relax your mind.";
}
}
class PhysicalWellness implements IPhysicalWellnessService {
getPhysicalWellnessAdvice(): string {
return "Make sure to exercise daily for at least 30 minutes.";
}
}
// HealthAdvice class that depends on services via interfaces
class HealthAdvice {
private mentalWellnessService: IMentalWellnessService;
private physicalWellnessService: IPhysicalWellnessService;
// Dependency injection via constructor
constructor(
mentalWellnessService: IMentalWellnessService,
physicalWellnessService: IPhysicalWellnessService
) {
this.mentalWellnessService = mentalWellnessService;
this.physicalWellnessService = physicalWellnessService;
}
// Method to get both mental and physical wellness advice
getHealthAdvice(): string {
return `${this.mentalWellnessService.getMentalWellnessAdvice()} Also, ${this.physicalWellnessService.getPhysicalWellnessAdvice()}`;
}
}
// Dependency injection
const mentalWellness: IMentalWellnessService = new MentalWellness();
const physicalWellness: IPhysicalWellnessService = new PhysicalWellness();
// Injecting services into the HealthAdvice class
const healthAdvice = new HealthAdvice(mentalWellness, physicalWellness);
console.log(healthAdvice.getHealthAdvice());
// Output: "Take time to meditate and relax your mind. Also, Make sure to exercise daily for at least 30 minutes."
在紧耦合的场景中,如果你今天stressLevel
在类中有一个属性MentalWellness
,明天决定将其更改为其他属性,则需要更新所有使用该属性的地方。这会带来大量的重构和维护挑战。
但是,通过依赖注入和使用接口,您可以避免这个问题。MentalWellness
通过构造函数传递依赖项(例如服务),具体的实现细节(例如stressLevel
属性)被抽象到接口后面。这意味着,只要接口保持不变,对属性或类的更改就不需要修改依赖类。这种方法可以确保代码松耦合、更易于维护和测试,因为您是在运行时注入所需的内容,而无需紧密耦合组件。