每个开发者都应该了解的十个高级TypeScript概念
TypeScript 是一种现代编程语言,因其增加了类型安全性,常被开发者优先选择。在这篇文章里,我将分享十个最重要的 TypeScript 概念,帮助你提升 TypeScript 编程技能。准备好了吗?那就开始吧。
1.泛型:使用泛型,我们可以创建可重用的类型,这将有助于处理今天和明天的数据。
泛型的示例:
我们可能希望在 Typescript 中创建一个函数,它接受某个类型的参数,并返回相同类型的值。
// 这是一个泛型函数,它接受一个类型为T的参数,并返回相同类型的值。
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"); // 可以,T 为 string
const numberValue = func(42); // 可以,T 为 number
// const booleanValue = func(true); // 不合法:类型 'boolean' 无法赋值给类型 'string | number'
// 说明:这里的 TypeScript 泛型类型约束表示 T 只能是 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') {
// 根据名字搜索:
console.log('根据名字搜索:', idOrName);
return this.items.find(user => user.name === idOrName);
} else if (typeof idOrName === 'number') {
// 根据ID搜索:
console.log('根据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); // 输出: { id: 1, name: "Alice" }
console.log(user2); // 输出: { 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;
}
// 更新姓名
updateName(newName: string): void {
this.name = newName;
}
// 更新年龄
updateAge(newAge: number): void {
this.age = newAge;
}
// 获取用户信息的方法
getUserDetails(): string {
return `用户: ${this.name}, ID: ${this.id}, 年龄: ${this.age}`;
}
}
// 使用用户信息类与User类型
const user: User = { id: 1, name: "Alice", age: 30 };
const userDetails = new UserDetails(user);
console.log(userDetails.getUserDetails()); // 输出: "用户: Alice, ID: 1, 年龄: 30"
// 更新用户信息
userDetails.updateName("Bob");
userDetails.updateAge(35);
console.log(userDetails.getUserDetails()); // 输出: "用户: Bob, ID: 1, 年龄: 35"
console.log(new UserDetails("30")); // 这将导致一个错误,因为传递了一个不符合User类型的参数
切换到全屏模式,退出全屏
5. 限制类型参数为传入的类型:有时候,我们希望某个参数类型能够依赖于传入的其他参数。听起来可能有点模糊,接下来通过下面这个例子来说明一下。
function getProperty<Type>(obj: Type, key: keyof Type) {
return obj[key];
}
let x = { a: 1, b: 2, c: 3 };
getProperty(x, "a"); // 正确
getProperty(x, "d"); // 错误:属性 'd' 无法赋值,因为它不在可接受的属性列表中。
全屏模式 退出全屏
6.条件类型:很多时候,我们希望类型能够是这两种之一。这时,我们通常会用到条件类型。
一个简单的例子是:
function func(param: number | boolean) {
return param;
}
console.log(func(2)); // 输出: 会显示 2
console.log(func("true")); // 错误: 不能将字符串作为参数传递
切换到全屏 退出全屏
一个稍微复杂一点的例子:
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">; // 错误:'email' 类型不能赋值给 'age' 或 'name',因为它们不是定义的属性。
切换到全屏 退出全屏
6.联合类型: 当我们想把多个类型合为一类时很有用,可以让特定类型继承来自多种类型的属性和行为。
来举一个有趣的例子:
// 定义每个领域福祉的类型
interface 心理福祉 {
正念练习: boolean;
压力值: number; // 1到10的等级
}
interface 身体健康 {
锻炼频率: string; // 例如,"每天", "每周"
睡眠时长: number; // 以小时为单位
}
interface 生产力 {
完成任务数: number;
专注度: number; // 1到10的等级
}
// 使用交集类型将三个领域合并为一个类型
type 健康身体 = 心理福祉 & 身体健康 & 生产力;
// 平衡健康身体的例子
const 个体: 健康身体 = {
正念练习: true,
压力值: 4,
锻炼频率: "每天",
睡眠时长: 7,
完成任务数: 15,
专注度: 8
};
// 输出信息
console.log(个体);
全屏模式,退出全屏
7. 关键字 infer
: 当我们需要根据条件确定特定类型或从类型中提取子类型,且条件满足时,关键字 infer
非常有用。
这是通用语法:
这是一个条件类型定义,类型参数T如果继承自SomeType,则推断为InferredType,否则为OtherType。
切换到全屏模式 结束全屏模式
例如:
type ReturnTypeOfPromise<T> = T extends Promise<infer U> ? U : number;
type Result = ReturnTypeOfPromise<Promise<string>>; // 结果是字符串
type ErrorResult = ReturnTypeOfPromise<number>; // ErrorResult 是 never
const result: Result = "Hello";
console.log(typeof result); // 输出: string
全屏模式,开启/关闭
8.类型变异性:这个概念讨论了子类型和超类型之间是如何相互关联的。
这种类型变异性分为两种类型:
协变:一个子类型可以在需要超类型的地方被使用。
让我们来看一个例子:
class Vehicle { }
class Car extends Vehicle { }
function getCar(): Car {
return new Car();
}
function getVehicle(): Vehicle {
return new Vehicle();
}
// 协变赋值(即 Car 可以赋值给 Vehicle 类型)
let car: Car = getCar();
let vehicle: Vehicle = getVehicle(); // 这是因为 Car 可以被视为一种特定的 Vehicle
切换到全屏模式 结束全屏模式
在上面的例子中,Car 类继承了 Vehicle 类的属性,因此将它赋值给预期是超类型的子类型是完全有效的。
逆变性(Contravariance):这是协变的对立面。我们在预期为子类型的地方使用超类型对象。
class Vehicle {
startEngine() {
console.log("车辆引擎启动");
}
}
class Car extends Vehicle {
honk() {
console.log("汽车鸣笛");
}
}
function processVehicle(vehicle: Vehicle) {
vehicle.startEngine(); // 这可以正常工作
// vehicle.honk(); // 错误:'honk' 不在类型 'Vehicle' 上存在
}
function processCar(car: Car) {
car.startEngine(); // 这可以正常工作,因为 Car 继承了 Vehicle
car.honk(); // 这可以正常工作,因为 'Car' 有 'honk'
}
let car: Car = new Car();
processVehicle(car); // 这可以正常工作,因为逆变(Car 可以作为 Vehicle 使用)
processCar(car); // 这也可以正常工作,这是因为 car 的类型是 Car
// 如果方法中包含特定子类型的行为,逆变会失败
全屏模式, 退出全屏
使用逆变时,我们得小心,不要尝试访问子类型特有的属性或方法,以免出错。
9. 思考: 这个概念是指在运行时确定变量类型。虽然 TypeScript 主要是在编译时做类型检查,我们仍然可以在运行时利用 TypeScript 的操作符来检查类型。
typeof
操作符:我们可以使用 typeof
操作符来获取变量的类型。
const num = 23;
console.log(typeof num); // "数字"
const flag = true;
console.log(typeof flag); // "布尔"
全屏模式 退出全屏
instanceof
操作符: instanceof
操作符可以用来判断对象是否为某类或者特定类型的实例。
class 汽车 {
model: string;
constructor(model: string) {
this.model = model;
}
}
const benz = new 汽车("梅赛德斯-奔驰");
console.log(benz instanceof 汽车); // true
进入全屏 退出全屏
我们可以使用第三方库来判断运行时的类型。
10.依赖注入: 依赖注入是一种模式,允许你将依赖项引入到组件中,而无需实际在其中创建或管理它们。虽然这可能看起来像在使用库,但它实际上并不一样,因为你不必通过CDN或API来安装或引入它。
乍一看,这可能也类似于为了代码复用而使用函数。因为两者都支持代码重用。然而,如果我们直接在组件之间使用函数,会导致它们之间紧密耦合。这意味着任何对函数或其逻辑的更改都可能影响到它的每个使用点。
依赖注入(Dependency Injection)通过解耦依赖项和使用它们的组件,从而使代码更易于维护和测试。
未使用依赖注入的示例
// 没有接口的健康相关服务类
class MentalWellness {
getMentalWellnessAdvice(): string {
return "花时间冥想,放松心理。";
}
}
class PhysicalWellness {
getPhysicalWellnessAdvice(): string {
return "每天至少锻炼30分钟,保持身体健康。";
}
}
// HealthAdvice 类直接创建服务实例
class HealthAdvice {
private mentalWellnessService: MentalWellness;
private physicalWellnessService: PhysicalWellness;
// 在构造函数中直接创建实例
constructor() {
this.mentalWellnessService = new MentalWellness();
this.physicalWellnessService = new PhysicalWellness();
}
// 获取身心健康建议
getHealthAdvice(): string {
return `${this.mentalWellnessService.getMentalWellnessAdvice()} 另外,${this.physicalWellnessService.getPhysicalWellnessAdvice()}`;
}
}
// 创建 HealthAdvice 实例,它会创建服务实例
const healthAdvice = new HealthAdvice();
console.log(healthAdvice.getHealthAdvice());
// 输出:"花时间冥想,放松心理。 另外,每天至少锻炼30分钟,保持身体健康。"
进入全屏模式,退出全屏模式
依赖注入的例子
// 带有“I”前缀的健康服务接口
interface IMentalWellnessService {
获取心理健康建议(): string;
}
interface IPhysicalWellnessService {
获取身体健康建议(): string;
}
// 服务的实现
class MentalWellness implements IMentalWellnessService {
获取心理健康建议(): string {
return "花时间冥想,放松你的心理。";
}
}
class PhysicalWellness implements IPhysicalWellnessService {
获取身体健康建议(): string {
return "每天至少运动30分钟。";
}
}
// 提供健康建议的HealthAdvice类
class HealthAdvice {
private mentalWellnessService: IMentalWellnessService;
private physicalWellnessService: IPhysicalWellnessService;
// 通过构造函数的依赖注入
constructor(
mentalWellnessService: IMentalWellnessService,
physicalWellnessService: IPhysicalWellnessService
) {
this.mentalWellnessService = mentalWellnessService;
this.physicalWellnessService = physicalWellnessService;
}
// 获取健康建议的方法
获取健康建议(): string {
return `${this.mentalWellnessService.获取心理健康建议()} 另外,${this.physicalWellnessService.获取身体健康建议()}`;
}
}
// 依赖注入
const mentalWellness: IMentalWellnessService = new MentalWellness();
const physicalWellness: IPhysicalWellnessService = new PhysicalWellness();
// 将服务注入到HealthAdvice类
const healthAdvice = new HealthAdvice(mentalWellness, physicalWellness);
console.log(healthAdvice.获取健康建议());
// 输出: "花时间冥想,放松你的心理。 另外,每天至少运动30分钟。"
切换到全屏模式。退出全屏
在紧密耦合的情境中,如果你今天在 MentalWellness
类中有一个 stressLevel
属性,而决定明天把它改成别的,那么你需要去更新所有用到它的地方。这会带来很多重构和维护上的问题。
然而,通过依赖注入和接口的使用,你可以避免这个问题。通过构造函数传递依赖项(如 MentalWellness
服务),具体的实现细节(如 stressLevel
属性)被隐藏在了接口后面。这意味着,只要接口不变,属性或类的变化就不会影响依赖的类。这种方法确保代码是松耦合的,更易于维护和测试。你可以在运行时注入所需的依赖项,而不会让组件紧密耦合。
共同学习,写下你的评论
评论加载中...
作者其他优质文章