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

TypeScript类型体操经典题目解析

2021-12-033.9k 阅读

一、基础类型推断

  1. 获取函数返回值类型 在TypeScript类型体操中,获取函数返回值类型是一个基础且常见的题目。例如,给定一个函数 add
function add(a: number, b: number): number {
    return a + b;
}

我们想要获取这个函数的返回值类型。可以使用TypeScript的 ReturnType 工具类型来实现:

type AddReturnType = ReturnType<typeof add>;
// AddReturnType 此时为 number 类型

ReturnType 工具类型接收一个函数类型作为参数,并返回该函数的返回值类型。这里通过 typeof add 获取函数 add 的类型,然后传递给 ReturnType 得到返回值类型。

  1. 获取对象属性类型 假设有一个对象 user
const user = {
    name: 'John',
    age: 30
};

我们要获取 user 对象中某个属性的类型。比如获取 name 属性的类型,可以这样做:

type UserNameType = typeof user['name'];
// UserNameType 为 string 类型

这里利用了TypeScript的索引访问类型。通过 typeof user 获取 user 对象的类型,然后使用 ['name'] 来访问 name 属性的类型。

二、条件类型与类型判断

  1. 简单条件类型示例 条件类型在TypeScript类型体操中非常重要。例如,我们定义一个条件类型 If
type If<C extends boolean, T, F> = C extends true? T : F;

这里 If 类型接收三个类型参数,第一个 C 是一个布尔类型的约束,TF 是两个普通类型。如果 Ctrue,则返回 T 类型,否则返回 F 类型。使用示例如下:

type Result1 = If<true, string, number>;
// Result1 为 string 类型
type Result2 = If<false, string, number>;
// Result2 为 number 类型
  1. 类型相等判断 有时候我们需要判断两个类型是否相等。可以通过条件类型来实现一个简单的类型相等判断工具类型 IsEqual
type IsEqual<A, B> = (<T>() => T extends A? 1 : 2) extends (<T>() => T extends B? 1 : 2)? true : false;

这里利用了函数类型的分布式条件类型特性。示例如下:

type IsStringEqual = IsEqual<string, string>;
// IsStringEqual 为 true
type IsStringNumberEqual = IsEqual<string, number>;
// IsStringNumberEqual 为 false

三、数组类型操作

  1. 获取数组元素类型 对于一个数组类型,我们经常需要获取其元素类型。例如,有一个数组 numbers
const numbers: number[] = [1, 2, 3];

获取其元素类型可以使用如下方式:

type NumberArrayElementType = numbers[number];
// NumberArrayElementType 为 number 类型

这里利用了TypeScript的索引类型查询。numbers 是一个数组类型,number 作为索引类型,表示数组的所有可能索引,这样就可以获取到数组元素类型。

  1. 数组长度相关类型操作 有时候我们想根据数组的长度来定义不同的类型。例如,定义一个类型 Length 来获取数组的长度类型:
type Length<T extends any[]> = T['length'];

使用示例:

const arr: [1, 2, 3] = [1, 2, 3];
type ArrLength = Length<typeof arr>;
// ArrLength 为 3

这里通过索引访问数组的 length 属性来获取数组的长度类型。

四、高级类型递归

  1. 类型递归实现链表 链表是数据结构中的经典概念,我们可以在TypeScript类型层面实现链表。首先定义链表节点类型:
type ListNode<T> = {
    value: T;
    next: ListNode<T> | null;
};

然后可以通过递归定义一个链表类型,例如:

type LinkedList<T> = ListNode<T> | null;

这里 LinkedList 类型表示一个链表,它可以是一个节点或者 null。如果是节点,节点又包含一个值和指向下一个节点的引用,下一个节点又是 ListNode<T> 或者 null,这样就通过类型递归实现了链表结构。

  1. 递归实现深度索引访问 假设我们有一个嵌套对象:
const nestedObject = {
    a: {
        b: {
            c: 'value'
        }
    }
};

我们想要通过类型递归实现深度索引访问,获取 c 的类型。定义一个类型 DeepIndex

type DeepIndex<T, K extends keyof any> = K extends keyof T
   ? T[K]
    : K extends `${infer First}.${infer Rest}`
       ? First extends keyof T
          ? DeepIndex<T[First], Rest>
           : never
        : never;

使用示例:

type DeepValueType = DeepIndex<typeof nestedObject, 'a.b.c'>;
// DeepValueType 为 string 类型

这里首先判断 K 是否是 T 的直接属性,如果是则返回对应属性类型。如果 K 是一个以点分隔的字符串,就通过字符串字面量类型的推断提取出第一个属性名 First 和剩余部分 Rest,然后递归调用 DeepIndex 获取深层属性的类型。

五、映射类型与属性操作

  1. 映射类型基础 映射类型允许我们基于现有的类型创建新的类型,通过对每个属性进行相同的转换。例如,将一个对象类型的所有属性变为只读:
type ReadonlyKeys<T> = {
    readonly [K in keyof T]: T[K];
};

假设有一个类型 User

type User = {
    name: string;
    age: number;
};

使用 ReadonlyKeys 类型:

type ReadonlyUser = ReadonlyKeys<User>;
// ReadonlyUser 的属性 name 和 age 都变为只读

这里通过 [K in keyof T] 遍历 User 类型的所有属性键 K,并为每个键 K 创建一个新的只读属性,属性值类型与原类型相同。

  1. 属性筛选与转换 有时候我们需要根据属性的类型筛选并转换属性。例如,只保留对象中字符串类型的属性,并将其值变为大写:
type FilterAndTransform<T> = {
    [K in keyof T as T[K] extends string? K : never]: T[K] extends string? Uppercase<T[K]> : never;
};

假设有一个类型 Data

type Data = {
    name: string;
    age: number;
    address: string;
};

使用 FilterAndTransform 类型:

type TransformedData = FilterAndTransform<Data>;
// TransformedData 只包含 name 和 address 属性,且值为大写

这里通过 as 关键字在遍历属性键 K 时进行筛选,如果属性值类型是 string 则保留该键,否则使用 never 过滤掉。同时,对于保留的字符串类型属性值,通过 Uppercase 工具类型将其转换为大写。

六、类型体操综合题目解析

  1. 实现 Omit 工具类型 Omit 工具类型用于从一个类型中移除指定的属性。例如,从 User 类型中移除 age 属性:
type User = {
    name: string;
    age: number;
    email: string;
};
type Omit<T, K extends keyof T> = {
    [P in keyof T as P extends K? never : P]: T[P];
};
type UserWithoutAge = Omit<User, 'age'>;
// UserWithoutAge 只包含 name 和 email 属性

这里通过 [P in keyof T as P extends K? never : P] 遍历 User 类型的所有属性键 P,如果 P 是要移除的属性键 K,则使用 never 过滤掉,否则保留该属性键,从而实现从类型中移除指定属性。

  1. 实现 Pick 工具类型 Pick 工具类型用于从一个类型中选取指定的属性。例如,从 User 类型中选取 nameemail 属性:
type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};
type UserInfo = Pick<User, 'name' | 'email'>;
// UserInfo 只包含 name 和 email 属性

这里通过 [P in K] 遍历要选取的属性键 K,并为每个键 P 创建一个属性,属性值类型与原 User 类型中对应属性相同,从而实现从类型中选取指定属性。

  1. 实现 Exclude 工具类型 Exclude 工具类型用于从一个联合类型中排除另一个联合类型中的类型。例如,从 string | number | boolean 中排除 number
type Exclude<T, U> = T extends U? never : T;
type NewType = Exclude<string | number | boolean, number>;
// NewType 为 string | boolean

这里通过条件类型判断,如果 T 中的某个类型是 U 中的类型,则返回 never,否则返回 T 中的类型,从而实现从联合类型中排除指定类型。

  1. 实现 Extract 工具类型 Extract 工具类型用于从一个联合类型中提取另一个联合类型中的类型。例如,从 string | number | boolean 中提取 number
type Extract<T, U> = T extends U? T : never;
type ExtractedType = Extract<string | number | boolean, number>;
// ExtractedType 为 number

这里通过条件类型判断,如果 T 中的某个类型是 U 中的类型,则返回该类型,否则返回 never,从而实现从联合类型中提取指定类型。

  1. 实现 NonNullable 工具类型 NonNullable 工具类型用于从一个类型中移除 nullundefined。例如,从 string | null | undefined 中移除 nullundefined
type NonNullable<T> = T extends null | undefined? never : T;
type CleanType = NonNullable<string | null | undefined>;
// CleanType 为 string

这里通过条件类型判断,如果 Tnull 或者 undefined,则返回 never,否则返回 T,从而实现从类型中移除 nullundefined

  1. 实现 Required 工具类型 Required 工具类型用于将一个对象类型中的所有属性变为必选。例如,将一个包含可选属性的 User 类型变为所有属性必选:
type User = {
    name?: string;
    age?: number;
};
type Required<T> = {
    [K in keyof T]-?: T[K];
};
type RequiredUser = Required<User>;
// RequiredUser 中的 name 和 age 属性都变为必选

这里通过 [K in keyof T]-? 表示移除属性的可选修饰符 ?,从而将所有属性变为必选。

  1. 实现 Partial 工具类型 Partial 工具类型用于将一个对象类型中的所有属性变为可选。例如,将一个所有属性必选的 User 类型变为所有属性可选:
type User = {
    name: string;
    age: number;
};
type Partial<T> = {
    [K in keyof T]?: T[K];
};
type PartialUser = Partial<User>;
// PartialUser 中的 name 和 age 属性都变为可选

这里通过 [K in keyof T]? 为每个属性添加可选修饰符 ?,从而将所有属性变为可选。

七、类型体操在实际项目中的应用

  1. 接口数据验证 在实际项目中,我们经常需要对接口返回的数据进行类型验证。例如,我们从后端获取一个用户信息接口,返回的数据类型如下:
interface UserResponse {
    id: number;
    name: string;
    age: number;
}

我们可以利用类型体操来确保前端接收到的数据符合这个类型。比如,定义一个类型断言函数:

function assertUserResponse(data: any): asserts data is UserResponse {
    if (typeof data.id!== 'number' || typeof data.name!=='string' || typeof data.age!== 'number') {
        throw new Error('Invalid user response data');
    }
}

这里通过类型体操相关的类型断言,确保传入的数据符合 UserResponse 类型。如果不符合,则抛出错误。

  1. 函数参数类型约束 在函数调用中,类型体操可以帮助我们更好地约束函数参数类型。例如,有一个函数 printUser 用于打印用户信息:
function printUser(user: { name: string; age: number }) {
    console.log(`Name: ${user.name}, Age: ${user.age}`);
}

通过类型体操,我们可以定义更严格的用户类型,然后将其作为函数参数类型,确保传入的参数符合要求。比如:

type User = {
    name: string;
    age: number;
    email: string;
};
function printUser(user: Pick<User, 'name' | 'age'>) {
    console.log(`Name: ${user.name}, Age: ${user.age}`);
}

这里使用 Pick 工具类型从 User 类型中选取 nameage 属性作为函数参数类型,这样就约束了函数只能接收包含这两个属性的对象。

  1. 组件属性类型定义 在前端框架(如React)中,组件的属性类型定义非常重要。例如,有一个 Button 组件:
interface ButtonProps {
    text: string;
    onClick: () => void;
    disabled?: boolean;
}

我们可以利用类型体操来对属性类型进行更灵活的处理。比如,定义一个 OptionalButtonProps 类型,使所有属性都变为可选:

type OptionalButtonProps = Partial<ButtonProps>;

这样在某些情况下,我们可以使用 OptionalButtonProps 类型来传递部分属性给 Button 组件。

八、类型体操与代码维护

  1. 类型一致性维护 在大型项目中,代码的类型一致性非常重要。类型体操可以帮助我们确保不同模块之间使用的类型是一致的。例如,有一个用户信息模块和订单模块,两个模块都可能使用到用户的基本信息类型。通过类型体操定义一个通用的用户基本信息类型:
type UserBaseInfo = {
    id: number;
    name: string;
};

然后在用户信息模块和订单模块中都使用这个类型,这样当用户基本信息类型需要修改时,只需要在一个地方修改 UserBaseInfo 类型定义,其他使用该类型的地方都会自动更新,从而维护了类型的一致性。

  1. 代码重构时的类型调整 当进行代码重构时,类型体操可以帮助我们更方便地调整类型。例如,原来有一个函数接收一个包含多个属性的对象作为参数:
function processData(data: { prop1: string; prop2: number; prop3: boolean }) {
    // 处理数据
}

现在重构代码,只需要 prop1prop2 属性。我们可以使用 Pick 工具类型来调整函数参数类型:

function processData(data: Pick<{ prop1: string; prop2: number; prop3: boolean }, 'prop1' | 'prop2'>) {
    // 处理数据
}

这样在不改变原有代码太多的情况下,通过类型体操实现了函数参数类型的调整,同时也保证了类型的正确性。

  1. 类型文档化与可维护性 良好的类型定义本身就是一种文档。通过类型体操定义的复杂类型,可以清晰地表达代码的意图。例如,定义一个复杂的表单数据类型:
type FormData = {
    username: string;
    password: string;
    age: number;
    address: {
        street: string;
        city: string;
        zipCode: string;
    };
};

这样的类型定义让其他开发者在阅读代码时,能够快速了解表单数据的结构,提高了代码的可维护性。同时,在进行代码修改时,类型体操可以帮助我们更准确地判断修改对类型的影响,减少潜在的类型错误。

九、避免类型体操中的常见错误

  1. 类型循环引用 在类型递归和映射类型等操作中,很容易出现类型循环引用的错误。例如:
type CircularType = {
    value: string;
    next: CircularType;
};
// 这里会报错,因为 CircularType 类型定义中存在循环引用

为了避免这种错误,要确保类型递归有终止条件。比如在链表类型定义中,使用 null 作为递归终止的条件:

type ListNode<T> = {
    value: T;
    next: ListNode<T> | null;
};
  1. 条件类型的逻辑错误 在编写条件类型时,逻辑错误也是常见的问题。例如,在实现 IsEqual 类型时,如果逻辑写错:
// 错误的 IsEqual 实现
type WrongIsEqual<A, B> = A extends B? true : false;

这个实现没有考虑到分布式条件类型的特性,对于一些复杂类型可能会给出错误的结果。正确的实现应该像前面介绍的那样,利用函数类型的分布式条件类型特性来确保准确的类型相等判断。

  1. 属性访问越界 在进行属性访问相关的类型操作时,可能会出现属性访问越界的错误。例如:
type SomeType = {
    prop1: string;
};
type ErrorType = SomeType['prop2'];
// 这里会报错,因为 SomeType 中不存在 prop2 属性

在进行属性访问类型操作时,要确保访问的属性确实存在于目标类型中,可以通过条件类型先进行属性存在性判断,避免这种错误。

  1. 工具类型使用不当 对于TypeScript提供的工具类型(如 ReturnTypePartial 等),如果使用不当也会导致问题。例如,将 ReturnType 应用于非函数类型:
type NotAFunction = string;
type ErrorReturnType = ReturnType<NotAFunction>;
// 这里会报错,因为 ReturnType 要求参数是函数类型

要正确使用工具类型,确保传入的参数符合工具类型的要求。

十、类型体操的拓展与未来发展

  1. 与其他前端技术的融合 随着前端技术的不断发展,TypeScript类型体操有望与更多其他技术融合。例如,在WebAssembly领域,TypeScript类型可以更好地与WebAssembly模块的接口进行映射,通过类型体操可以更方便地定义和验证WebAssembly模块的输入输出类型。在React Native等移动开发框架中,类型体操可以进一步优化组件属性和状态的类型定义,提高代码的健壮性和可维护性。

  2. 对复杂数据结构的支持增强 未来,TypeScript可能会进一步增强对复杂数据结构的类型支持,类型体操也会随之发展。例如,对于图结构、树结构等复杂数据结构,可能会有更便捷的类型定义和操作方式。通过类型体操,开发者可以更轻松地实现对这些复杂数据结构的遍历、查询等操作的类型安全。

  3. 类型推导的智能化提升 TypeScript的类型推导能力可能会变得更加智能化。类型体操也会受益于这种提升,使得开发者在编写复杂类型时,不需要手动进行过多的类型标注,TypeScript可以根据上下文更准确地推导类型。这将大大减少类型体操中的冗余代码,提高开发效率。

  4. 与后端语言的类型交互 随着全栈开发的流行,TypeScript类型体操可能会在与后端语言的类型交互方面有更多发展。例如,与Java、Python等后端语言的类型系统进行更好的对接,通过类型体操实现前后端类型的自动转换和验证,减少因前后端类型不一致导致的错误。