typeScript工具泛型

150 阅读7分钟

工具泛型

在项目中使用一些工具泛型可以提高我们的开发效率,少写很多类型定义。下面来看看有哪些常见的工具泛型,以及其使用方式。

1. Partial

应用场景:通过登记身份证、手机号、车牌号的其中一条信息,就可以领取奖品

Partial 作用是将传入的属性变为可选项。适用于对类型结构不明确的情况。它使用了两个关键字:keyof 和 in,先来看看他们都是什么含义。keyof 可以用来取得接口的所有 key 值:

interface IPerson {
  name: string;
  age: number;
  score: number;
}
type T = keyof IPerson;
// T 类型为: "name" | "age" | "score"

in 关键字可以遍历枚举类型:

type Person = "name" | "age" | "number";
type Obj = {
  [p in Keys]: any;
};
// Obj类型为: { name: any, age: any, number: any }

keyof 可以产生联合类型, in 可以遍历枚举类型, 所以经常一起使用, 下面是 Partial 工具泛型的定义:

/**
 * Make all properties in T optional
 * 将T中的所有属性设置为可选
 */
type Partial<T> = {
  [P in keyof T]?: T[P];
};

这里,keyof T 获取 T 所有属性名, 然后使用 in 进行遍历, 将值赋给 P, 最后 T[P] 取得相应属性的值。中间的?就用来将属性设置为可选。 使用示例如下:

interface IPerson {
  name: string;
  age: number;
  height: number;
}

const person: Partial<IPerson> = {
  name: "zhangsan";
}

实践案例

import React from "react";
type Props = {};
type UserRecord = {
  idNumber: string;
  carNumber: string;
  phoneNumber: string;
};
const PartialDemo = (props: Props) => {
  let uinfo: Partial<UserRecord> = {
    //此处如果不使用Partial,则uinfo内部不许录入所有字段
    phoneNumber: "13966667777",
  };
  return (
    <div>
      PartialDemo---登记用户的身份证、或车牌、或手机
      {Object.keys(uinfo).map((item) => {
        return (
          <p>
            {item}---{uinfo[item as keyof UserRecord]} //此处需要对item进行断言
          </p>
        );
      })}
    </div>
  );
};

export default PartialDemo;

2. Required

Required 的作用是将传入的属性变为必选项,和上面的工具泛型恰好相反,其声明如下:

/**
 * Make all properties in T required
 * 将T中的所有属性设置为必选
 */
type Required<T> = {
  [P in keyof T]-?: T[P];
};

可以看到,这里使用-?将属性设置为必选,可以理解为减去问号。适用形式和上面的 Partial 差不多:

interface IPerson {
  name?: string;
  age?: number;
  height?: number;
}

const person: Required<IPerson> = {
  name: "zhangsan";
  age: 18;
  height: 180;
}

实践案例

import React from "react";
enum PersonEnum { //定义enum
  name = "姓名",
  score = "分数",
  age = "年龄",
}
type PersonKey = keyof typeof PersonEnum; //获取enum的所有键名
interface Person {
  name?: string;
  score?: number;
  age?: number;
}

const RequireDemo = () => {
  let stu: Required<Person> = {
    //此处如果不使用Required,则内部所有字段都是可选的
    name: "三丰",
    score: 100,
    age: 18,
  };
  return (
    <div>
      RequireDemo
      {Object.keys(stu).map((key) => {
        return (
          <p>
            {PersonEnum[key as PersonKey]} -- {stu[key as PersonKey]}
          </p>
        );
      })}
    </div>
  );
};

export default RequireDemo;

3. Readonly

将 T 类型的所有属性设置为只读(readonly),构造出来类型的属性不能被再次赋值。Readonly 的声明形式如下:

/**
 * Make all properties in T readonly
 */
type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

使用示例如下:

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

const person: Readonly<IPerson> = {
  name: "zhangsan",
  age: 18,
};

person.age = 20; //  Error: cannot reassign a readonly property

可以看到,通过 Readonly 将 IPerson 的属性转化成了只读,不能再进行赋值操作。

4. Pick<T, K extends keyof T>

从 T 类型中挑选部分属性 K 来构造新的类型。它的声明形式如下:

/**
 * From T, pick a set of properties whose keys are in the union K
 */
type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};

使用示例如下:

interface IPerson {
  name: string;
  age: number;
  height: number;
}

const person: Pick<IPerson, "name" | "age"> = {
  name: "zhangsan",
  age: 18,
};

5. Record<K extends keyof any, T>

Record 用来构造一个类型,其属性名的类型为 K,属性值的类型为 T。这个工具泛型可用来将某个类型的属性映射到另一个类型上,下面是其声明形式:

/**
 * Construct a type with a set of properties K of type T
 */
type Record<K extends keyof any, T> = {
  [P in K]: T;
};

使用示例如下:

interface IPageinfo {
  title: string;
}

type IPage = "home" | "about" | "contact";
//约束page数据包格式,必须以IPage类型作为键名,以IPageinfo类型作为键值
const page: Record<IPage, IPageinfo> = {
  about: { title: "about" },
  contact: { title: "contact" },
  home: { title: "home" },
};

实践案例:RequiredDemo.tsx 代码改造

import React from "react";
enum PersonEnum { //定义enum
  name = "姓名",
  score = "分数",
  age = "年龄",
}
type PersonKey = keyof typeof PersonEnum; //获取enum的所有键名
type PersonValue = string;
// interface Person {  //省略此处的接口定义
//   name?:string,
//   score?:number,
//   age?:number
// }

const RequireDemo = () => {
  let stu: Record<PersonKey, PersonValue> = {
    //以PersonKey约束键名,以PersonValue约束键值类型
    name: "三丰",
    score: "100",
    age: "18",
  };
  return (
    <div>
      RequireDemo
      {Object.keys(stu).map((key) => {
        return (
          <p>
            {PersonEnum[key as PersonKey]} -- {stu[key as PersonKey]}
          </p>
        );
      })}
    </div>
  );
};

export default RequireDemo;

6. Exclude<T, U>

Exclude 就是从一个联合类型中排除掉属于另一个联合类型的子集,下面是其声明的形式:

/**
 * Exclude from T those types that are assignable to U
 */
type Exclude<T, U> = T extends U ? never : T;

使用示例如下:

interface IPerson {
  name: string;
  age: number;
  height: number;
}

const person: Exclude<IPerson, "age" | "sex"> = {
  name: "zhangsan";
  height: 180;
}

7. Omit<T, K extends keyof any>

上面的 Pick 和 Exclude 都是最基础基础的工具泛型,很多时候用 Pick 或者 Exclude 还不如直接写类型更直接。而 Omit 就基于这两个来做的一个更抽象的封装,它允许从一个对象中剔除若干个属性,剩下的就是需要的新类型。下面是它的声明形式:

/**
 * Construct a type with the properties of T except for those in type K.
 */
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

使用示例如下:

interface IPerson {
  name: string;
  age: number;
  height: number;
}

const person: Omit<IPerson, "age" | "height"> = {
  name: "zhangsan";
}

8. ReturnType

ReturnType 会返回函数返回值的类型,其声明形式如下:

/**
 * Obtain the return type of a function type
 */
type ReturnType<T extends (...args: any) => any> = T extends (
  ...args: any
) => infer R
  ? R
  : any;

使用示例如下:

function foo(type): boolean {
  return type === 0;
}

type FooType = ReturnType<typeof foo>;

这里使用 typeof 是为了获取 foo 的函数签名,等价于 (type: any) => boolean。 import _ as React from 'react' import _ as ReactDOM from 'react-dom' 复制代码 import React from "react"; import ReactDOM from "react-dom"; 复制代码"compilerOptions": { // 允许默认从没有默认导出的模块导入。 "allowSyntheticDefaultImports": true, } 复制代码

其他

1. Types or Interfaces?

我们可以使用 types 或者 Interfaces 来定义类型吗,那么该如何选择他俩呢?建议如下:

  • 在定义公共 API 时(比如编辑一个库)使用 interface,这样可以方便使用者继承接口,这样允许使用最通过声明合并来扩展它们;
  • 在定义组件属性(Props)和状态(State)时,建议使用 type,因为 type 的约束性更强。

interface 和 type 在 ts 中是两个不同的概念,但在 React 大部分使用的 case 中,interface 和 type 可以达到相同的功能效果,type 和 interface 最大的区别是:type 类型不能二次编辑,而 interface 可以随时扩展:

interface Animal {
  name: string;
}

// 可以继续在原属性基础上,添加新属性:color
interface Animal {
  color: string;
}

type Animal = {
  name: string;
};
// type类型不支持属性扩展
// Error: Duplicate identifier 'Animal'
type Animal = {
  color: string;
};

type 对于联合类型是很有用的,比如:type Type = TypeA | TypeB。而 interface 更适合声明字典类行,然后定义或者扩展它。

2. 类型断言

类型断言(Type Assertion)可以用来手动指定一个值的类型。在 React 项目中,断言还是很有用的,。有时候推断出来的类型并不是真正的类型,很多时候我们可能会比 TS 更懂我们的代码,所以可以使用断言(使用 as 关键字)来定义一个值得类型。 来看下面的例子:

const getLength = (target: string | number): number => {
  if (target.length) {
    // error 类型"string | number"上不存在属性"length"
    return target.length; // error  类型"number"上不存在属性"length"
  } else {
    return target.toString().length;
  }
};

当 TypeScript 不确定一个联合类型的变量到底是哪个类型时,就只能访问此联合类型的所有类型里共有的属性或方法,所以现在加了对参数 target 和返回值的类型定义之后就会报错。这时就可以使用断言,将 target 的类型断言成 string 类型:

const getStrLength = (target: string | number): number => {
  if ((target as string).length) {
    return (target as string).length;
  } else {
    return target.toString().length;
  }
};

需要注意,类型断言并不是类型转换,断言成一个联合类型中不存在的类型是不允许的。 再来看一个例子,在调用一个方法时传入参数: 这里就提示我们这个参数可能是 undefined,而通过业务知道这个值是一定存在的,所以就可以将它断言成数字:data?.subjectId as number 除此之外,上面所说的标签类型、组件类型、时间类型都可以使用断言来指定给一些数据,还是要根据实际的业务场景来使用。 经验:使用类型断言能解决项目中的很多报错~