ts类型体操实战

ts类型体操实战

Posted by XIY on April 1, 2025

前言

在 TypeScript 的学习与实践中,理解和掌握各种类型工具及自定义类型操作是提升代码质量与开发效率的关键。本文将深入探讨 PartialReadonly 等常见工具类型的实现原理,并结合 lodashget 方法进行类型体操实践,以加深对 TypeScript 类型系统的理解。

ts 相关 api

TypeScript 提供了一系列强大的内置工具类型,这些类型在处理对象类型、联合类型等方面发挥着重要作用,极大地增强了类型系统的表达能力。以下是一些常见的工具类型:

  1. Partial<T>:将类型 T 的所有属性变为可选属性。常用于在创建对象时,某些属性不一定需要立即提供的场景。例如,在更新用户信息的接口中,可能只需要提供部分需要更新的字段,而不是全部用户信息字段。
  2. Required<T>:与 Partial 相反,将类型 T 的所有属性变为必选属性。当需要确保对象包含所有预期属性时使用。
  3. Readonly<T>:将类型 T 的所有属性变为只读属性。一旦对象被赋值,其属性值就不能再被修改,适用于保护对象数据不被意外更改的场景。
  4. Record<K, T>:创建一个具有指定键类型 K 和值类型 T 的新对象类型。常用于声明属性名还未确定的接口类型,或者将一个类型的属性映射为另一个类型。
  5. Pick<T, K>:从类型 T 中选择指定属性 K 形成新类型。在需要从一个已有的复杂类型中抽取部分属性组成新类型时非常有用。
  6. Omit<T, K>:与 Pick 相反,从类型 T 中排除指定属性 K 形成新类型。用于去除对象中不需要的属性。
  7. Exclude<T, U>:从类型 T 中排除可以赋值给类型 U 的类型。常用于在联合类型中剔除某些特定类型。
  8. Extract<T, U>:提取类型 TU 的共有类型。与 Exclude 操作相反。

Partial 实现

Partial 的作用是将传入类型 T 的所有属性变为可选。其实现原理基于 TypeScript 的映射类型和 keyof 操作符。

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

解释:

  1. keyof T 获取类型 T 的所有属性名组成的联合类型。
  2. [P in keyof T] 使用 in 关键字对 keyof T 得到的联合类型进行遍历,P 代表每个属性名。
  3. ?: 表示将属性变为可选。
  4. T[P] 表示属性 P 在类型 T 中的值类型。

例如:

interface User {
    name: string;
    age: number;
    email: string;
}
type PartialUser = Partial<User>;
// 此时 PartialUser 类型为:
// {
//     name?: string;
//     age?: number;
//     email?: string;
// }
const partialUser: PartialUser = { name: 'John Doe' }; // 合法,因为属性都是可选的

Readonly 实现

Readonly 用于将类型 T 的所有属性变为只读,即一旦对象被赋值,其属性值不能再被修改。实现如下:

type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

解释:

  1. 同样使用 keyof T 获取类型 T 的所有属性名联合类型。
  2. readonly 关键字将属性标记为只读。
  3. [P in keyof T] 遍历属性名联合类型,P 代表每个属性名。
  4. T[P] 表示属性 P 在类型 T 中的值类型。

例如:

interface Person {
    name: string;
    job: string;
    age: number;
}
type ReadonlyPerson = Readonly<Person>;
// ReadonlyPerson 类型为:
// {
//     readonly name: string;
//     readonly job: string;
//     readonly age: number;
// }
const readonlyPerson: ReadonlyPerson = { name: 'Jane', job: 'Engineer', age: 30 };
// readonlyPerson.name = 'New Name'; // 报错,因为 name 属性是只读的

loadsh get方法配置类型体操

lodashget 方法是一个非常实用的工具函数,用于从对象中获取指定路径的值。在 TypeScript 中,我们可以通过类型体操来为 get 方法配置更精确的类型,以增强代码的类型安全性。

import { get } from 'lodash';

type DeepKeyOf<T> = T extends Record<string, any>
  ? {
        [k in keyof T]: k extends string ? k | `${k}.${DeepKeyOf<T[k]>}` : never;
    }[keyof T]
  : never;

type DeepPathType<T, K extends string> = T extends Record<string, any>
  ? K extends keyof T
        ? T[K]
        : K extends `${infer A}.${infer B}`
              ? A extends keyof T
                    ? DeepPathType<T[A], B>
                    : never
              : never
  : never;

function myGet<T extends object, K extends DeepKeyOf<T>>(
    obj: T,
    path: K
): DeepPathType<T, K> {
    return get(obj, path) as DeepPathType<T, K>;
}

const obj = {
    name: {
        hhh: 12321,
        nest: {
            age: 'fdafa',
            hh: true,
        },
    },
};

let a = get(obj, 'name.hhh'); // let a: any,lodash get 方法默认返回 any 类型
let res = myGet(obj, 'name.hhh'); // let res: number,通过类型体操,明确返回值类型为 number
let res1 = myGet(obj, 'name.nest.hh'); // let res1: boolean,明确返回值类型为 boolean

解释:

  1. DeepKeyOf<T>:该类型用于生成对象 T 的所有深层键的联合类型。通过递归的方式,将对象的所有层级的键都包含进来,以支持深层路径的访问。
  2. DeepPathType<T, K>:根据给定的对象类型 T 和路径字符串 K,获取对应路径的值的类型。通过层层解析路径字符串,递归地获取深层属性的类型。
  3. myGet 函数:接受一个对象 obj 和路径 path,路径 path 的类型被限制为 DeepKeyOf<T>,确保传入的路径是对象实际存在的深层路径。函数返回值类型为 DeepPathType<T, K>,即根据路径获取到的值的精确类型。这样,在使用 myGet 函数时,TypeScript 能够准确推断返回值的类型,避免了使用 lodash 原始 get 方法时返回值类型为 any 的问题,提高了代码的类型安全性和可维护性。

通过以上对 PartialReadonly 实现原理的分析以及 lodash get 方法类型体操的实践,我们对 TypeScript 的类型系统有了更深入的理解和掌握,能够编写出更健壮、类型安全的代码。