TypeScript体操基础 - 类型操作符

59 阅读11分钟

1. 引言

在TypeScript的世界里,高级关键词不仅仅是语法糖,它们是构建复杂和高效类型系统的工具箱。从实现强大的类型推导、精确的类型约束,到类型的转换和操作。我们来一探究竟,看看这些关键词如何在TypeScript中扮演着不可或缺的角色。

2. 关键词一览

  • extends:通常见于泛型约束,它可以用来条件地选择类型。但别小看了这个关键词,它的魔法远不止于此。
  • keyof:这个关键词能够获取一个对象类型的所有键作为联合类型。简单来说,就是让你知道这个对象有哪些“钥匙”。
  • typeof:如果你想要获取一个对象或变量的类型,typeof就是你的好朋友。它可以帮你在类型世界里复制现实世界的对象。
  • infer:这是一个在条件类型中使用的关键词,用于推断出期望的类型。想象一下,你是一个侦探,infer就是帮助你解开类型之谜的线索。
  • in:用于遍历枚举类型或进行类型映射,in关键词让你能够逐个访问联合类型中的每一个类型。
  • as:类型断言让你能够告诉编译器:“相信我,我知道我在做什么”。它允许你将一个类型断言为另一个类型。

3. 关键词详解

3.1. extends:条件类型和泛型约束

  • 用途extends关键词在TypeScript中主要用于两个方面:泛型约束和条件类型。
    • 泛型约束:通过extends,我们可以限制泛型T必须符合某个类型,这为类型安全提供了保障。
    • 条件类型extends还可以用于创建基于条件的类型,这类类型会根据条件表达式的结果产生不同的类型。
  • 示例
// 泛型约束
function identity<T extends { name: string }>(arg: T): T {
    console.log(arg.name);
    return arg;
}

// 条件类型
type IsString<T> = T extends string ? "yes" : "no";
type Result = IsString<"hello">;  // Result类型为"yes"

3.2. keyof:索引类型查询

  • 用途keyof关键词用于获取某种类型的所有键,其结果为这些键名称的联合类型。这使得我们能够以类型安全的方式引用对象的键,进而实现更加灵活的类型操作。
  • 示例
interface Person {
    name: string;
    age: number;
}

type PersonKeys = keyof Person;  // "name" | "age"

// 使用PersonKeys
function getProperty<T, K extends keyof T>(obj: T, key: K) {
    return obj[key];  // 在这里,key的类型安全地限制在了T的所有键中
}

const person: Person = { name: "Alice", age: 30 };
const name = getProperty(person, "name");  // 正确
// const error = getProperty(person, "notExist");  // 错误:类型“"notExist"”的参数不能赋给类型“"name" | "age"”的参数。

3.3. typeof:从实例推导出类型

  • 用途typeof关键词在TypeScript中用于获取一个变量或对象的类型。这对于在不重新声明类型的情况下复用已有数据结构的类型信息特别有用。
  • 示例
let sample = { name: "Tom", age: 30 };

// 使用typeof获取sample对象的类型
type SampleType = typeof sample;

// 现在我们可以使用SampleType来声明新的变量,而不需要重新定义类型
const another: SampleType = {
    name: "Jerry",
    age: 25
};

// 这种方法特别适合于当复杂对象或变量已经定义,而我们希望在不引入额外类型声明的情况下重用类型信息。

3.4. infer:在条件类型中推断类型

  • 用途infer关键词在条件类型声明中使用,允许我们在条件类型的分支中推断出类型变量。这是一种强大的类型推导手段,使得类型操作更加灵活和智能。
  • 示例
// 定义一个条件类型,用于获取函数返回值的类型
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;

// 使用示例
function getString(): string {
    return "hello";
}

function getNumber(): number {
    return 123;
}

type StringReturnType = ReturnType<typeof getString>;  // string
type NumberReturnType = ReturnType<typeof getNumber>;  // number

// 通过infer R,我们能够在不具体指定函数返回类型的情况下,推断出函数的返回类型。
// 这对于处理高阶函数或者类型封装时特别有用。

3.5. in:映射类型中的属性遍历

  • 用途in关键词用于定义映射类型时,对联合类型进行遍历,生成新的类型。这使得我们可以基于已有的类型动态地创建新类型,极大地增加了类型系统的灵活性和表达力。
  • 示例
type Keys = "a" | "b" | "c";

// 使用in遍历Keys联合类型,为每个键生成一个string类型的属性
type DynamicObject = {
    [P in Keys]: string;
};

// DynamicObject的类型等价于:
// {
//   a: string;
//   b: string;
//   c: string;
// }

// 这种方式特别适合于需要根据一组固定的键动态生成类型的场景。
// 例如,当我们想要确保一个对象包含某个键集合的同类型值时,就可以使用这种方法。

3.6. as:类型断言

  • 用途as关键词用于类型断言,允许开发者告诉编译器他们已经知道某个值的类型。类型断言可以用于绕过TypeScript的类型检查器,在开发者确定代码安全的情况下,这是一种非常有用的特性。
  • 示例
// 假设我们有一个any类型的变量,我们知道它实际上是一个string类型
let someValue: any = "this is a string";

// 使用as进行类型断言
let strLength: number = (someValue as string).length;

// 另一种类型断言方式是使用“尖括号语法”,但在JSX中只能使用as语法
let someOtherValue: any = "this is another string";
let anotherStrLength: number = (<string>someOtherValue).length;

// 类型断言不是类型转换,它不会改变值的类型,而是告诉TypeScript编译器如何理解该值的类型。

4. 高级类型函数实现

TypeScript的类型系统不仅强大而且灵活,提供了丰富的内置类型工具,使得开发者可以在不牺牲类型安全的前提下实现复杂的类型操作。通过结合使用TypeScript的高级关键词,我们可以实现自己的高级类型函数,进一步提升代码的健壮性和可维护性。

4.1. Partial<T>Required<T>

4.1.1. Partial<T>

用途:将某个类型的所有属性变为可选。

实现

type Partial<T> = {
    [P in keyof T]?: T[P];
};

示例

interface Person {
    name: string;
    age: number;
}

// 使用Partial使Person接口中的属性都变为可选
type PartialPerson = Partial<Person>;

4.1.2. Required<T>

用途:将某个类型的所有属性变为必选。

实现

type Required<T> = {
    [P in keyof T]-?: T[P];
};

示例

interface Person {
    name?: string;
    age?: number;
}

// 使用Required使Person接口中的属性都变为必选
type RequiredPerson = Required<Person>;

4.2. Pick<T, K>Omit<T, K>

4.2.1. Pick<T, K>

用途:从类型T中选取一组属性K来构造类型。

实现

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

示例

interface Person {
    name: string;
    age: number;
    location: string;
}

// 使用Pick选取Person中的'name'和'age'属性
type PersonBasics = Pick<Person, 'name' | 'age'>;

4.2.2. Omit<T, K>

用途:从类型T中排除一组属性K来构造类型。

实现

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

示例

interface Person {
    name: string;
    age: number;
    location: string;
}

// 使用Omit排除Person中的'location'属性
type PersonWithoutLocation = Omit<Person, 'location'>;

4.3. Record<K, T>

用途Record<K, T> 类型可以创建一个对象类型,其属性键为K,属性值为T。这对于需要一个索引签名,但又想保持键和值类型明确的场景非常有用。

实现

type Record<K extends keyof any, T> = {
  [P in K]: T;
};

示例

// 使用Record创建一个地图,其中键是国家名,值是该国的首都
type CountryCapitalMap = Record<string, string>;

const countryCapitals: CountryCapitalMap = {
  USA: "Washington, D.C.",
  China: "Beijing",
  India: "New Delhi",
};

4.4. Exclude<T, U>

用途Exclude<T, U> 类型用于从类型T中排除那些可赋值给U的类型,使得类型更加精确。

实现

type Exclude<T, U> = T extends U ? never : T;

示例

type PrimitiveTypes = string | number | boolean;
type NonBooleanTypes = Exclude<PrimitiveTypes, boolean>;

// NonBooleanTypes是string | number

4.5. Extract<T, U>

用途Extract<T, U> 类型用于从类型T中提取那些可赋值给U的类型,它与Exclude正好相反。

实现

type Extract<T, U> = T extends U ? T : never;

示例

type PossibleValues = "a" | "b" | "c" | 1 | 2;
type StringValues = Extract<PossibleValues, string>;

// StringValues是"a" | "b" | "c"

这三个类型Record<K, T>Exclude<T, U>Extract<T, U> 展示了TypeScript内置类型如何使得类型操作更加灵活和强大。通过合理运用这些类型,我们可以在保证代码类型安全的同时,提高代码的可读性和易维护性。

4.6. NonNullable

用途NonNullable 用于从类型T中排除nullundefined,使得类型更加严格和安全。

实现

type NonNullable<T> = T extends null | undefined ? never : T;

示例

type MaybeNumber = number | null | undefined;
type JustNumber = NonNullable<MaybeNumber>;

// JustNumber是number,已经排除了null和undefined

4.7. Readonly

用途Readonly 用于将类型T的所有属性设置为只读,防止赋值后对这些属性的修改。

实现

type MyReadonly<T> = {
    readonly [key in keyof T]: T[key];
};

示例

interface User {
  name: string;
  age: number;
}

const user: Readonly<User> = { name: "John", age: 30 };
// user.name = "Jane"; // 错误:name是只读属性,不能修改。

4.8. ReturnType

用途ReturnType 用于获取函数T的返回类型,这对于依赖于其他函数返回值的类型定义非常有用。

实现

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

示例

function getUser() {
  return { name: "John", age: 30 };
}

type User = ReturnType<typeof getUser>;

// User的类型是{name: string; age: number;}

4.9. Parameters

用途Parameters 用于获取函数T的参数类型,以元组的形式返回。这在模拟函数调用或者高阶函数中特别有用。

实现

type Parameters<T> = T extends (...args: infer P) => any ? P : never;

示例

function greet(name: string, age: number): string {
  return `Hello, ${name}. You are ${age}.`;
}

type GreetParameters = Parameters<typeof greet>;

// GreetParameters的类型是[string, number]

4.10. ConstructorParameters

用途ConstructorParameters 用于获取构造函数类型的所有参数类型,同样以元组的形式返回。这对于创建类实例或者高阶类型约束时非常有用。

实现

type ConstructorParameters<T> = T extends new (...args: infer P) => any ? P : never;

示例

class Person {
  constructor(public name: string, public age: number) {}
}

type PersonConstructorParameters = ConstructorParameters<typeof Person>;

// PersonConstructorParameters的类型是[string, number]

5. 实用案例分析

5.1. 动态属性访问

在处理复杂对象时,我们经常需要根据某些条件动态地访问对象的属性。使用keyof和类型保护,我们可以安全地实现这一功能。

interface Employee {
  id: number;
  name: string;
  department: string;
}

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const employee: Employee = {
  id: 1,
  name: "John Doe",
  department: "HR",
};

// 安全访问
const name: string = getProperty(employee, "name");
console.log(name); // 输出: John Doe

5.2. 条件类型在API响应中的应用

当调用一个返回不同类型数据的API时,使用条件类型可以帮助我们根据响应的内容声明不同的类型,从而更精确地处理数据。

type ApiResponse<T> = T extends "user" ? User : T extends "settings" ? Settings : never;

function fetchApi<T extends "user" | "settings">(endpoint: T): ApiResponse<T> {
  // 模拟API调用
  // 实际应用中,这里会是异步请求逻辑
  return undefined as any;
}

// 根据不同的参数,返回值类型会自动匹配
const user: User = fetchApi("user");
const settings: Settings = fetchApi("settings");

5.3. 使用映射类型改造旧代码

假设我们有一个现存的接口,需要将其所有属性变为可选的新接口,以支持部分更新功能。我们可以使用映射类型Partial来轻松实现这一需求,而不需要手动声明每个属性。

interface OldInterface {
  prop1: string;
  prop2: number;
  prop3: boolean;
}

type PartialInterface = Partial<OldInterface>;

// 现在PartialInterface的所有属性都是可选的
const update: PartialInterface = {
  prop1: "new value",
  // prop2和prop3是可选的,可以不提供
};

5.4. 利用高级类型进行错误处理

在处理函数返回值时,尤其是在异步操作中,我们通常需要处理成功和失败两种情况。利用TypeScript的高级类型,我们可以创建一个包含两种状态的返回类型,使错误处理逻辑更加清晰。

type Result<T> = { success: true; value: T } | { success: false; error: Error };

function safeParse<T>(json: string): Result<T> {
  try {
    return { success: true, value: JSON.parse(json) };
  } catch (error) {
    return { success: false, error: error instanceof Error ? error : new Error(String(error)) };
  }
}

const result = safeParse<{ name: string }>('{"name":"John"}');
if (result.success) {
  console.log(result.value.name); // 安全访问
} else {
  console.error(result.error.message);
}

6. 性能考虑与最佳实践

6.1. 使用interface和type的最佳实践

虽然interfacetype在很多情况下可以互换使用,但它们各自都有最适合的应用场景。一般来说,当定义对象的形状时,优先使用interface,因为它更适合声明合并和扩展。而对于需要使用联合类型、交叉类型或其他复杂类型操作时,则应该使用type

6.2. 限制递归类型的深度

在定义递归类型时,需要注意限制递归的深度,以避免编译器资源耗尽。TypeScript有一定的递归深度限制来防止无限递归,但是深度过大的递归依然会导致编译缓慢。在可能的情况下,尝试寻找非递归的类型定义方案,或者通过条件类型限制递归深度。

7. 总结

前文介绍了TS的关键词以及一些常用的内置类型。在日常开发中可以灵活使用这些类型和关键词,掌握了这些技巧可以灵活的打出一套类型组合拳,在类型体操的道路上越走越远~