TypeScript 中几个小技巧

1. 泛型的用法

本来以为在开发中基本用不到泛型,没想到今天还是碰到了。公司的项目是用 React 写的,其中需要使用 useSelector 的 hook 从 redux 获取数据,代码如下:

const { auditFilterOptions, auditRecords, auditTableLoading } = useSelector(state => {
	auditFilterOptions: state.getIn(["resource", "auditFilterOptions"]),
	auditRecords: state.getIn(["resource", "auditRecords"]),
	auditTableLoading: state.getIn(["auditTableLoading ", "auditTableLoading "])
})

在使用 TypeScript 的时候,提示需要给形参 state 声明类型。但是我们知道,state 是一个对象,上面挂载了 getIn 方法。因此,在声明类型的时候,势必要声明 getIn 方法的入参和返回值类型。但是在上面代码中,每一个 store 的字段,对应的数据类型是不一样的,这导致每次调用 getIn 方法的返回值可类型可能是不一样的:

type AuditFilterOptions = {
	limitMinTime: string;
	operateUsers: string[];
	serviceTypes: string[];
	tenants: string[];
}

type AuditRecords = {
	total: number;
	page: number;
	rows: {
		operateTime: string;
		operateMessage: string;
		operateUser: string;
		operateType: string;
	}[];
}

type AuditTableLoading = boolean;

之前同事在开发的时候,直接简单粗暴:

interface State {
	getIn: Function;
}

这样操作,虽然类型检查不会报错,但是无疑丢失了类型信息,返回类型直接被推导为 any ,无法获取到对象属性的类型了。那么需要怎么样才可以保留类型信息呢?

在 TypeScript 中可以给一个变量声明多个类型,能否用在这个场景呢?

interface State {
	getIn(a: string[]): AuditFilterOptions | AuditRecords | AuditTableLoading;
}

经过测试发现,可以给变量、函数形参声明多个类型,但不能给函数返回值声明多个类型。以上代码不会报错,但是 TS 始终只能匹配到第一种类型。那么是否还有其它方案呢?

之前在 Java 里面用过函数重载,就是一个函数名可以有不同的类型定义,编译器通过不同的入参及入参类型调用对应的函数。在 TS 中也提供了函数重载机制:

interface State {
	getIn(a: string[]): AuditFilterOptions;
	getIn(a: string[]): AuditRecords;
	getIn(a: string[]): AuditTableLoading;
}

但是实际测试发现,由于入参都一样,编译器没办法区分类型,始终匹配到的是第一个返回值类型。

那么怎么样才能实现匹配不同类型的返回值呢?想到了泛型。泛型就是参数化类型,可以传入类型参数。按照这个思路,state 的类型可以这样声明:

interface State {
	getIn<T>(a: string[]): T;
}

对应 useSelector 就可以这样使用:

const { auditFilterOptions, auditRecords, auditTableLoading } = useSelector((state: State) => {
	auditFilterOptions: state.getIn<AuditFilterOptions>(["resource", "auditFilterOptions"]),
	auditRecords: state.getIn<AuditRecords>(["resource", "auditRecords"]),
	auditTableLoading: state.getIn<AuditTableLoading>(["auditTableLoading ", "auditTableLoading "])
})

然后也尝试过这个方案:

interface State<T> {
	getIn(a: string[]): T;
}

但这样的话,useSelector 就得拆开写了,太麻烦,还是上面的写法简单:

const { auditFilterOptions } = useSelector((state: State<AuditFilterOptions>) => {
	auditFilterOptions: state.getIn(["resource", "auditFilterOptions"])
})
const { auditRecords } = useSelector((state: State<AuditRecords>) => {
	auditRecords: state.getIn(["resource", "auditRecords"]),
})
const { auditTableLoading } = useSelector((state: State<AuditTableLoading>) => {
	auditTableLoading: state.getIn(["auditTableLoading ", "auditTableLoading "])
})

这边要注意下,在 TSX 文件中,在箭头函数下使用泛型,<T> 可能会被识别为 JSX 标签:
请添加图片描述
这个时候需要显式告知编译器:

const foo = <T extends {}>(arg: T) => arg;

2. 联合类型与交叉类型

3. 类型保护

在定义类型的时候,会遇到对象属性或者函数形参类型可能为 undefined 或者 null 的情况。如果想访问这些类型上面的属性,编译器会报错:

// 对象属性不存在
const props: {name?: string} = {};
console.log(props.name.length); // Object is possibly 'undefined'
// 函数形参不存在
const fn = (s?: string) => s.length; // Object is possibly 'undefined'
// 函数形参可能为 null
const fn2 = (s: string | null) => s.length; // Object is possibly 'null'

当 TypeScript 不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型里共有的属性或方法。

但在某些场景中,我们确实需要在还不确定类型的时候访问其中一个类型的属性和方法,这个时候就需要使用类型断言,即手动告诉编译器这是哪种类型。

例如在下面的代码中,我们将 s 断言为 string 就不会报错了:

const fn = (s?: string) => (s as string).length;

除了使用 as ,还可以用下面的方式:

const fn = (s?: string) => (<string>s).length;

上面的代码中,<string> 可能会被识别为 JSX 标签,还是推荐使用 as

上面的方式可以用于各种类型断言。另外,还可以使用 ! 进行非空断言,用来强调对应的元素是非null|undefined的:

const fn = (s?: string) => s!.length;

需要注意,类型断言仅仅是让 TS 编译器在类型检查时不报错,让开发者自己决定是哪种类型。因此在下面的代码中,如果我们不给 fn 传递参数,最终代码执行的时候还是会报错:

const fn = (s?: string) => (s as string).length;
fn(); // Cannot read property 'length' of undefined

顺带提一下,在 Java 中是不允许指定联合类型的,自然也就不存在函数参数可能为空的情况。在 Java 中解决函数可选参数的方案是函数重载,同样在 TS 中也可以使用函数重载,从而避免使用类型断言:

const fn = (s: string) => s.length;
const fn = () => /* do something */

4. 字典类型

在对象中经常会遇到属性数量不固定的情况,这个时候无论用 interface 还是 type 都无法准确描述对象的类型,在这种情况下就可使用字典类型。字典用于描述任意数量属性、但具有相同类型的对象,类似于 Java 中的 Map

type Dict = {
	[key: string]: MyTypeHere;
}

// or
type Dict = Record<string, MyTypeHere>;

此外字典还可以将对象进行扩展:

type Dict = {
	name: string;
	age: string;
	[key: string]: MyTypeHere;
}

可以使用动态属性名来定义类型:

interface ChinaMobile {
  name: string;
  website: string;
}
 
interface ChinaMobileList {
  // 动态属性
  [phone: string]: ChinaMobile
}

5. 类型遍历

当你已知某个类型范围的时候,可以使用 inkeyof 来遍历类型,例如上面的 ChinaMobile 例子,我们可以使用 in 来约束属性名必须为三家运营商之一:

type ChinaMobilePhones = '10086' | '10010' | '10000'
 
interface ChinaMobile {
  name: string;
  website: string;
}
 
// 只能 type 使用, interface 无法使用
type ChinaMobileList = {
  // 遍历属性
  [phone in ChinaMobilePhones]: ChinaMobile
}

例如需要定义很多同一种类型的 key ,下面这样写比较麻烦:

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

这种情况下可以用 in 关键字遍历类型,得到的结果与上面完全一致:

type Keys = "name" | "age" | "sex";
type User = {
    [key in Keys]: string;
}

我们也可以用 keyof 来约定方法的参数:

export type keys = {
  name: string;
  appId: number;
  config: object;
}
 
class Application {
  // 参数和值约束范围
  set<T extends keyof keys>(key: T, val: keys[T])
  get<T extends keyof keys>(key: T): keys[T]
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值