头图

这些都不知道,还敢说你会TypeScript?

前言

大家好,我是倔强青铜三。是一名热情的软件工程师,我热衷于分享和传播IT技术,致力于通过我的知识和技能推动技术交流与创新,欢迎关注我,微信公众号:倔强青铜三。

TypeScript 是一种现代编程语言,由于其类型安全性,通常比 JavaScript 更受欢迎。在本文中,我将分享11个 TypeScript 概念,这些概念将帮助你提升 TypeScript 编程技能。你准备好了吗?让我们开始吧。

1. 泛型

使用泛型,我们可以创建可重用的类型,这对于处理今天的数据以及未来的数据都很有帮助。

示例

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. 泛型接口

泛型接口在定义对象、类或函数的合同时非常有用,这些合同需要与多种类型一起工作。它们允许你定义一个蓝图,该蓝图可以适应不同的数据类型,同时保持结构的一致性。

示例

// 带有类型参数 T 和 U 的泛型接口
interface Repository<T, U> {
    items: T[];           // 类型 T 的项目数组
    add(item: T): void;  // 添加类型 T 的项目的函数
    getById(id: U): T | undefined; // 通过类型 U 的 ID 获取项目的函数
}

// 为 User 实体实现 Repository 接口
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') {
            // 如果 idOrName 是字符串,则按名称搜索
            console.log('Searching by name:', idOrName);
            return this.items.find(user => user.name === idOrName);
        } else if (typeof idOrName === 'number') {
            // 如果 idOrName 是数字,则按 ID 搜索
            console.log('Searching by id:', idOrName);
            return this.items.find(user => user.id === idOrName);
        }
        return undefined; // 如果没有找到匹配项,则返回 undefined
    }
}

// 用法
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;
    }

    // 获取用户详细信息的方法
    getUserDetails(): string {
        return `User: ${this.name}, ID: ${this.id}, Age: ${this.age}`;
    }

    // 更新用户名的方法
    updateName(newName: string): void {
        this.name = newName;
    }

    // 更新用户年龄的方法
    updateAge(newAge: number): void {
        this.age = newAge;
    }
}

// 使用 User 类型的 UserDetails 类
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"

// 更新用户详细信息
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"'.

7. 交叉类型

当我们希望将多个类型组合成一个类型时,这些类型非常有用,允许特定类型从其他多个类型继承属性和行为。

示例

// 定义每个健康领域的类型

interface MentalWellness {
    mindfulnessPractice: boolean;
    stressLevel: number; // 1 到 10 的等级
}

interface PhysicalWellness {
    exerciseFrequency: string; // 例如,"daily", "weekly"
    sleepDuration: number; // 小时数
}

interface Productivity {
    tasksCompleted: number;
    focusLevel: number; // 1 到 10 的等级
}

// 使用交叉类型将这三个领域合并为一个类型
type HealthyBody = MentalWellness & PhysicalWellness & Productivity;

// 一个身心健康平衡的人的示例
const person: HealthyBody = {
    mindfulnessPractice: true,
    stressLevel: 4,
    exerciseFrequency: "daily",
    sleepDuration: 7,
    tasksCompleted: 15,
    focusLevel: 8
};

// 显示信息
console.log(person);

8. 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'

9. 类型变异

这个概念讨论了子类型和超类型如何相互关联。

协变:可以在期望超类型的地方使用子类型。

示例

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

逆变:与协变相反,我们在期望子类型的地方使用超类型。

示例

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

// If you expect specific subtype behavior in the method, contravariance will fail

10. 反射

这个概念涉及在运行时确定变量的类型。虽然 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

我们还可以使用第三方库在运行时确定类型。

11. 依赖注入

依赖注入是一种模式,它允许你将代码引入组件中,而无需在其中实际创建或管理它。它不同于使用库,因为你不需要通过 CDN 或 API 安装或导入它。

示例

不使用依赖注入的示例

// 与接口无关的健康相关服务类
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 HealthAdvice {
    private mentalWellnessService: MentalWellness;
    private physicalWellnessService: PhysicalWellness;

    // 在类构造函数中直接创建实例
    constructor() {
        this.mentalWellnessService = new MentalWellness();
        this.physicalWellnessService = new PhysicalWellness();
    }

    // 获取身心健康建议的方法
    getHealthAdvice(): string {
        return `${this.mentalWellnessService.getMentalWellnessAdvice()} Also, ${this.physicalWellnessService.getPhysicalWellnessAdvice()}`;
    }
}

// 创建 HealthAdvice 实例,该实例本身会创建服务实例
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."

使用依赖注入的示例

// 带有 "I" 前缀的健康相关服务接口
interface IMentalWellnessService {
    getMentalWellnessAdvice(): string;
}

interface IPhysicalWellnessService {
    getPhysicalWellnessAdvice(): string;
}

// 服务的实现
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 HealthAdvice {
    private mentalWellnessService: IMentalWellnessService;
    private physicalWellnessService: IPhysicalWellnessService;

    // 通过构造函数进行依赖注入
    constructor(
        mentalWellnessService: IMentalWellnessService,
        physicalWellnessService: IPhysicalWellnessService
    ) {
        this.mentalWellnessService = mentalWellnessService;
        this.physicalWellnessService = physicalWellnessService;
    }

    // 获取身心健康建议的方法
    getHealthAdvice(): string {
        return `${this.mentalWellnessService.getMentalWellnessAdvice()} Also, ${this.physicalWellnessService.getPhysicalWellnessAdvice()}`;
    }
}

// 依赖注入
const mentalWellness: IMentalWellnessService = new MentalWellness();
const physicalWellness: IPhysicalWellnessService = new PhysicalWellness();

// 将服务注入到 HealthAdvice 类中
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."

在紧密耦合的场景中,如果今天在 MentalWellness 类中有一个 stressLevel 属性,并且你决定明天将其更改为其他内容,则需要更新所有使用它的地方。这可能导致大量的重构和维护挑战。

然而,通过依赖注入和接口的使用,你可以避免这个问题。通过将依赖项(如 MentalWellness 服务)通过构造函数传递,特定的实现细节(如 stressLevel 属性)被隐藏在接口后面。这意味着,只要接口保持不变,属性的更改或类的更改就不需要修改依赖类。这种方法确保了代码的松散耦合、更易于维护,并且更易于测试,因为你在运行时注入了所需的内容,而没有紧密耦合组件。


倔强青铜三
21 声望0 粉丝