MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

TypeScript类型查询操作符深度解析

2022-07-281.5k 阅读

一、TypeScript 类型查询操作符概述

在 TypeScript 的类型系统中,类型查询操作符是一组强大的工具,用于在类型层面进行信息的提取和操作。它们允许开发者在类型定义时,以一种动态的方式获取和转换类型的相关信息。这些操作符为构建复杂且灵活的类型系统提供了坚实的基础,无论是在大型项目中进行模块间的类型交互,还是在编写可复用的库时确保类型的一致性和安全性,都有着不可或缺的作用。

TypeScript 中的类型查询操作符主要包括 keyoftypeofinExcludeExtractNonNullableReturnTypeInstanceType 等。每一个操作符都有其独特的用途,下面我们将逐一深入探讨。

二、keyof 操作符

2.1 keyof 基本用法

keyof 操作符用于获取对象类型的所有键的联合类型。其语法形式为 keyof Type,其中 Type 必须是对象类型。例如:

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

type PersonKeys = keyof Person;
// PersonKeys 等价于 'name' | 'age'

在上述代码中,通过 keyof Person 获取到了 Person 接口中所有键组成的联合类型 'name' | 'age'。这在很多场景下非常有用,比如当我们需要根据对象的键来进行操作时,可以使用这个联合类型来确保类型安全。

2.2 keyof 与索引类型

keyof 操作符与索引类型密切相关。索引类型允许我们通过一个索引类型参数来访问对象类型中的属性值类型。结合 keyof,可以实现类型安全的属性访问。例如:

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

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

const person: Person = { name: 'John', age: 30 };
const name = getProperty(person, 'name');
// name 的类型为 string

getProperty 函数中,K extends keyof T 确保了 key 参数只能是 T 类型对象的合法键。T[K] 则获取了对应键的值的类型。这样,通过 keyof 操作符,我们实现了类型安全的属性访问。

2.3 keyof 应用场景

keyof 在很多实际应用场景中都发挥着重要作用。例如,在实现一个通用的对象映射函数时,可以使用 keyof 来确保映射的属性都是源对象的合法键。

interface User {
    id: number;
    username: string;
}

function mapUserProperties<T extends User, K extends keyof T>(user: T, keys: K[]): Pick<T, K> {
    const result: Partial<T> = {};
    keys.forEach(key => {
        result[key] = user[key];
    });
    return result as Pick<T, K>;
}

const user: User = { id: 1, username: 'testuser' };
const mappedUser = mapUserProperties(user, ['id']);
// mappedUser 的类型为 Pick<User, 'id'>,即 { id: number }

在上述代码中,mapUserProperties 函数接受一个 User 类型的对象和一个由 User 对象键组成的数组。通过 keyof 确保了 keys 数组中的元素都是 User 对象的合法键,从而保证了类型安全。

三、typeof 操作符

3.1 typeof 在类型层面的用法

typeof 操作符在 JavaScript 中用于获取变量或表达式的运行时类型。而在 TypeScript 中,typeof 操作符被扩展用于获取变量的类型。其语法形式为 typeof identifier,其中 identifier 是一个变量名。例如:

const num = 10;
type NumType = typeof num;
// NumType 等价于 number

这里通过 typeof num 获取到了变量 num 的类型 number。这种方式使得我们可以在类型定义中复用变量的类型,增强了类型系统的灵活性。

3.2 typeof 与函数类型

typeof 操作符对于获取函数的类型也非常有用。通过 typeof 可以获取函数的参数类型和返回值类型,进而在类型层面进行操作。例如:

function add(a: number, b: number): number {
    return a + b;
}

type AddFunctionType = typeof add;
// AddFunctionType 等价于 (a: number, b: number) => number

function callFunction<T extends (...args: any[]) => any>(func: T, ...args: Parameters<T>): ReturnType<T> {
    return func(...args);
}

const result = callFunction(add, 1, 2);
// result 的类型为 number

在上述代码中,首先通过 typeof add 获取了 add 函数的类型 (a: number, b: number) => number。然后在 callFunction 函数中,利用 Parameters<T>ReturnType<T>(这两个操作符后面会详细介绍)结合 typeof 获取的函数类型,实现了类型安全的函数调用。

3.3 typeof 的常见应用场景

typeof 常用于创建与已有变量或函数类型一致的新类型。比如在实现一个日志记录函数时,可能需要根据已有函数的类型来定义日志记录函数的参数类型,以确保记录的参数与原函数参数一致。

function printMessage(message: string) {
    console.log(message);
}

function logFunctionCall<T extends (...args: any[]) => any>(func: T, ...args: Parameters<T>) {
    const funcName = func.name;
    const argStr = args.map(arg => JSON.stringify(arg)).join(', ');
    console.log(`Calling ${funcName} with args: [${argStr}]`);
    return func(...args);
}

const loggedResult = logFunctionCall(printMessage, 'Hello, world!');
// loggedResult 的类型与 printMessage 的返回值类型一致,即 void

在上述代码中,logFunctionCall 函数利用 typeof 获取 func 的类型,通过 Parameters<T> 获取其参数类型,从而实现了安全的函数调用记录。

四、in 操作符

4.1 in 操作符用于遍历联合类型

在 TypeScript 中,in 操作符用于遍历联合类型。它通常与映射类型一起使用,通过对联合类型中的每个成员进行操作,生成新的类型。其语法形式为 Key in UnionType,其中 Key 是一个类型变量,UnionType 是一个联合类型。例如:

type Colors ='red' | 'green' | 'blue';

type ColorObject = {
    [K in Colors]: string;
};
// ColorObject 等价于 { red: string; green: string; blue: string; }

在上述代码中,通过 [K in Colors] 遍历了 Colors 联合类型中的每个成员,并为每个成员定义了一个 string 类型的值,从而生成了 ColorObject 类型。

4.2 in 与映射类型的结合应用

映射类型允许我们通过对已有类型的属性进行操作,创建新的类型。in 操作符在其中起到了关键的遍历作用。例如,我们可以对一个对象类型的所有属性进行类型转换。

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

type StringifyUser = {
    [K in keyof User]: string;
};
// StringifyUser 等价于 { id: string; name: string; }

这里通过 keyof User 获取 User 接口的所有键,再利用 in 操作符遍历这些键,将每个属性的值类型都转换为 string,生成了 StringifyUser 类型。

4.3 in 的高级应用场景

在实现一些复杂的类型转换或条件类型时,in 操作符也经常发挥作用。例如,我们可以根据对象属性的类型,有条件地转换属性类型。

interface Data {
    id: number;
    name: string;
    isActive: boolean;
}

type ConditionalStringify<T> = {
    [K in keyof T]: T[K] extends string | number? string : T[K];
};

type ConditionalData = ConditionalStringify<Data>;
// ConditionalData 等价于 { id: string; name: string; isActive: boolean; }

在上述代码中,ConditionalStringify 类型利用 in 操作符遍历 Data 接口的所有属性,通过条件类型判断,如果属性类型是 stringnumber,则将其转换为 string,否则保持原类型。

五、Exclude 操作符

5.1 Exclude 基本概念与用法

Exclude 操作符用于从一个联合类型中排除另一个联合类型中的成员,生成一个新的联合类型。其语法形式为 Exclude<T, U>,其中 TU 都是联合类型。例如:

type Numbers = 1 | 2 | 3 | 4 | 5;
type EvenNumbers = 2 | 4;

type OddNumbers = Exclude<Numbers, EvenNumbers>;
// OddNumbers 等价于 1 | 3 | 5

在上述代码中,Exclude<Numbers, EvenNumbers>Numbers 联合类型中排除了 EvenNumbers 联合类型中的成员,得到了 OddNumbers 联合类型。

5.2 Exclude 在类型过滤中的应用

Exclude 在实际开发中常用于类型过滤。比如,在处理函数参数类型时,可能需要排除一些不合法的类型。

function handleNumbers(numbers: Exclude<number, string>) {
    // 这里 numbers 只能是 number 类型,排除了 string 类型
    numbers.forEach(num => {
        console.log(num);
    });
}

handleNumbers([1, 2, 3]);
// handleNumbers(['a', 'b']); // 报错,类型不匹配

在上述代码中,Exclude<number, string> 确保了 handleNumbers 函数的参数只能是 number 类型,排除了 string 类型,从而保证了函数调用的类型安全。

5.3 Exclude 与其他操作符的组合使用

Exclude 可以与其他类型查询操作符组合使用,以实现更复杂的类型操作。例如,结合 keyof 可以从对象类型的键中排除某些键。

interface Product {
    id: number;
    name: string;
    price: number;
    description: string;
}

type ExcludedKeys = Exclude<keyof Product, 'id' | 'price'>;
// ExcludedKeys 等价于 'name' | 'description'

type ExcludedProduct = Pick<Product, ExcludedKeys>;
// ExcludedProduct 等价于 { name: string; description: string; }

在上述代码中,首先通过 Exclude<keyof Product, 'id' | 'price'>Product 接口的键中排除了 'id''price',得到 ExcludedKeys。然后利用 Pick 类型从 Product 接口中选取 ExcludedKeys 对应的属性,生成了 ExcludedProduct 类型。

六、Extract 操作符

6.1 Extract 基本概念与用法

Extract 操作符与 Exclude 操作符相反,它用于从一个联合类型中提取出另一个联合类型中的成员,生成一个新的联合类型。其语法形式为 Extract<T, U>,其中 TU 都是联合类型。例如:

type AllFruits = 'apple' | 'banana' | 'cherry' | 'date';
type TropicalFruits = 'banana' | 'date';

type ExtractedTropicalFruits = Extract<AllFruits, TropicalFruits>;
// ExtractedTropicalFruits 等价于 'banana' | 'date'

在上述代码中,Extract<AllFruits, TropicalFruits>AllFruits 联合类型中提取出了 TropicalFruits 联合类型中的成员,得到了 ExtractedTropicalFruits 联合类型。

6.2 Extract 在类型筛选中的应用

Extract 在实际开发中常用于类型筛选。比如,在处理一个包含多种类型数据的数组时,可能需要筛选出特定类型的数据。

function filterArray<T, U extends T>(array: T[], type: U): Extract<T, U>[] {
    return array.filter(item => item === type) as Extract<T, U>[];
}

const numbers = [1, 2, 3, 4, 5];
const filteredNumbers = filterArray(numbers, 2);
// filteredNumbers 的类型为 [2],即 Extract<number, 2>[]

在上述代码中,filterArray 函数接受一个数组和一个特定类型的值,通过 Extract<T, U> 筛选出数组中与特定类型值相同的元素,返回一个包含筛选后元素的数组。

6.3 Extract 与其他操作符的组合应用

Extract 同样可以与其他类型查询操作符组合使用。例如,结合 typeof 可以从函数返回值类型的联合类型中提取出特定类型。

function getValue(): string | number | boolean {
    return Math.random() > 0.5? 'test' : 1;
}

type StringOrNumber = string | number;
type ExtractedReturnType = Extract<ReturnType<typeof getValue>, StringOrNumber>;
// ExtractedReturnType 等价于 string | number

在上述代码中,首先通过 typeof getValue 获取 getValue 函数的返回值类型,然后利用 Extract 从返回值类型中提取出 StringOrNumber 联合类型中的成员,得到 ExtractedReturnType 类型。

七、NonNullable 操作符

7.1 NonNullable 基本概念与用法

NonNullable 操作符用于从类型中排除 nullundefined。其语法形式为 NonNullable<T>,其中 T 是任意类型。例如:

type MaybeNumber = number | null | undefined;
type DefiniteNumber = NonNullable<MaybeNumber>;
// DefiniteNumber 等价于 number

在上述代码中,NonNullable<MaybeNumber>MaybeNumber 类型中排除了 nullundefined,得到了 DefiniteNumber 类型。

7.2 NonNullable 在处理可能为空值的类型时的应用

在实际开发中,经常会遇到一些可能返回 nullundefined 的值。NonNullable 操作符可以帮助我们在类型层面处理这种情况,确保后续操作的安全性。例如:

function getElementById(id: string): HTMLElement | null {
    return document.getElementById(id);
}

function handleElement(element: NonNullable<ReturnType<typeof getElementById>>) {
    element.classList.add('active');
}

const element = getElementById('myElement');
if (element) {
    handleElement(element);
}

在上述代码中,getElementById 函数可能返回 null。通过 NonNullable<ReturnType<typeof getElementById>> 获取了排除 null 后的 HTMLElement 类型,在 handleElement 函数中可以安全地对元素进行操作。在调用 handleElement 前,通过 if (element) 进行了空值检查,确保了类型安全。

7.3 NonNullable 与映射类型的结合应用

NonNullable 与映射类型结合可以对对象类型的所有属性进行空值排除。例如:

interface User {
    id: number | null;
    name: string | undefined;
}

type NonNullableUser = {
    [K in keyof User]: NonNullable<User[K]>;
};
// NonNullableUser 等价于 { id: number; name: string; }

在上述代码中,利用 in 操作符遍历 User 接口的所有属性,通过 NonNullable<User[K]> 对每个属性类型进行空值排除,生成了 NonNullableUser 类型。

八、ReturnType 操作符

8.1 ReturnType 基本概念与用法

ReturnType 操作符用于获取函数类型的返回值类型。其语法形式为 ReturnType<T>,其中 T 必须是函数类型。例如:

function add(a: number, b: number): number {
    return a + b;
}

type AddReturnType = ReturnType<typeof add>;
// AddReturnType 等价于 number

在上述代码中,通过 ReturnType<typeof add> 获取了 add 函数的返回值类型 number

8.2 ReturnType 在函数组合与类型安全中的应用

ReturnType 在函数组合和确保类型安全方面非常有用。例如,当我们有一系列函数,后一个函数的输入依赖于前一个函数的输出时,可以使用 ReturnType 来确保类型匹配。

function fetchData(): Promise<{ data: string }> {
    return Promise.resolve({ data: 'test data' });
}

function processData(data: ReturnType<typeof fetchData>['data']): number {
    return data.length;
}

fetchData().then(data => {
    const result = processData(data);
    console.log(result);
});

在上述代码中,processData 函数的输入类型依赖于 fetchData 函数的返回值类型中的 data 属性类型。通过 ReturnType<typeof fetchData>['data'] 确保了类型的一致性,从而保证了函数调用的类型安全。

8.3 ReturnType 与泛型函数的结合应用

在泛型函数中,ReturnType 同样可以发挥重要作用。例如,我们可以定义一个通用的函数包装器,保持原函数的返回值类型。

function wrapFunction<T extends (...args: any[]) => any>(func: T): (...args: Parameters<T>) => ReturnType<T> {
    return function (...args) {
        console.log('Before function call');
        const result = func.apply(this, args);
        console.log('After function call');
        return result;
    };
}

function multiply(a: number, b: number): number {
    return a * b;
}

const wrappedMultiply = wrapFunction(multiply);
const result = wrappedMultiply(2, 3);
// result 的类型为 number,与 multiply 函数的返回值类型一致

在上述代码中,wrapFunction 函数接受一个泛型函数 func,通过 ReturnType<T>Parameters<T> 确保了返回的包装函数的参数类型和返回值类型与原函数一致。

九、InstanceType 操作符

9.1 InstanceType 基本概念与用法

InstanceType 操作符用于获取构造函数类型的实例类型。其语法形式为 InstanceType<T>,其中 T 必须是构造函数类型。例如:

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

type PersonInstance = InstanceType<typeof Person>;
// PersonInstance 等价于 Person

在上述代码中,通过 InstanceType<typeof Person> 获取了 Person 类的实例类型 Person

9.2 InstanceType 在处理类实例类型时的应用

在实际开发中,当我们需要操作类的实例,但又希望在类型层面进行安全的操作时,InstanceType 操作符就派上用场了。例如,我们可以定义一个函数,接受 Person 类的实例,并对其属性进行操作。

function printPersonInfo(person: InstanceType<typeof Person>) {
    console.log(`Name: ${person.name}, Age: ${person.age}`);
}

const person = new Person('John', 30);
printPersonInfo(person);

在上述代码中,printPersonInfo 函数的参数类型通过 InstanceType<typeof Person> 确保为 Person 类的实例类型,从而保证了对 person 对象属性操作的类型安全。

9.3 InstanceType 与泛型类的结合应用

在泛型类的场景下,InstanceType 同样适用。例如:

class Container<T> {
    constructor(public value: T) {}
}

function getContainerValue<T>(container: InstanceType<typeof Container<T>>): T {
    return container.value;
}

const numberContainer = new Container(10);
const value = getContainerValue(numberContainer);
// value 的类型为 number

在上述代码中,Container 是一个泛型类,getContainerValue 函数通过 InstanceType<typeof Container<T>> 获取了 Container<T> 类实例的类型,确保了对 container 对象操作的类型安全,并正确返回了 value 的类型。

通过对这些 TypeScript 类型查询操作符的深度解析,我们可以看到它们在构建复杂且安全的类型系统中所发挥的强大作用。合理运用这些操作符,可以大大提高代码的可读性、可维护性以及类型安全性,为大型项目和可复用库的开发提供坚实的基础。在实际开发过程中,开发者应根据具体的业务需求,灵活组合使用这些操作符,以实现高效、安全的编程。