工具泛型
在项目中使用一些工具泛型可以提高我们的开发效率,少写很多类型定义。下面来看看有哪些常见的工具泛型,以及其使用方式。
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
除此之外,上面所说的标签类型、组件类型、时间类型都可以使用断言来指定给一些数据,还是要根据实际的业务场景来使用。
经验:使用类型断言能解决项目中的很多报错~