故事是这样的, 小红最近入职了一家新公司,摸鱼没几天主管就分了个需求,工作量不大,于是小红开始熟练的crud了的起来。
不太爽的是项目里开启了noImplicitAny ,anyscript
大法失效了,小红心里也明白,一直any
下去也不是个事,等后面在好好系统的学习下Typescript,可是在这次联调接口的时候却出现了一个类型的问题,来看小红是怎么解决的吧。
这次的需求是对商品界面进行增删改查,小红根据后端接口字段熟练的定义起了Product
类型,然后根据业务进行编码
ts
interface Product {
id: string;
name: string;
createDate: string,
price: number;
description: string;
// ...
}
// code something...
联调接口的时候,后台采用的是restful风格的接口,在定义修改函数的时候,小红给参数id
、productInfo
分别标注了string
和Product
类型。
ts
const updateProduct = (id: string, productInfo: Product) => {
// fetch api update product
};
// 更新name属性
updateProduct('1233', { // error 报错
name: "Excel从入门到精通",
});
// 更新price属性
updateProduct('1233', { // error 报错
price: 59.00,
});
// 更新description属性
updateProduct('1233', { // error 报错
description: "一本非常优秀的Excel书籍",
});
// 更新name和price属性
updateProduct('1233', { // error 报错
name: "Excel从入门到精通",
price: 59.00,
});
// 更新name和description属性
updateProduct('1233', { // error 报错
name: "Excel从入门到精通",
description: "一本非常优秀的Excel书籍",
});
前后端联调修改采用的是 [patch] 请求方法,只需要传递更新资源的一部分,所以当调用updateProduct
函数只传递一部分数据的时候Typescript报错了。
小红开始犯了难,这咋整,还是any香,但是用不了any好气,小红抬头望向天花板,思索了一会,根据以往学习的知识,重新定义了一个类型。
ts
interface OptionalProduct {
name?: string;
createDate?: string,
price?: number;
description?: string;
}
const updateProduct = (id: string, productInfo: OptionalProduct) => {
// fetch api update product
};
Typescript的类型检查通过!关键代码中都有类型标注,非常完美! 等完成了所有代码并自测后,小红开始git add git commit
提交代码,把code review
的链接发给了组长进行review
, 过了一会组长发来了一条消息:
"小红,OptionalProduct这个类型可以用但是扩展性不好,万一后面Product添加了新字段,OptionalProduct是不是也要添加新字段,你可以了解下Typescript提供的Partial 和Omit类型来重新构造OptionalProduct, 不会的再来问我"。
于是小红根据提示的信息开始了对Partial
和Omit
的学习:
Partial<Type>
- 构造一个将
Type
的所有属性设置为可选的类型。
ts
type Partial<T> = {
[P in keyof T]?: T[P];
};
Example:
ts
interface Product {
name: string;
createDate: string,
price: number;
description: string;
// ...
}
function updateProduct(fieldsToUpdate: Partial<Product>) {
// do something
}
// ok
updateProduct({
description: "一本非常优秀的Excel书籍",
});
// ok
updateProduct({
name: "Excel从入门到精通",
price: 59.00,
});
Exclude<UnionType, ExcludedMembers>
- 通过从
UnionType
中排除可分配给ExcludedMembers
的所有联合成员来构造类型。
ts
type Exclude<T, U> = T extends U ? never : T;
Example:
ts
type ExcludeId = Exclude<'id' | 'name' | 'createDate' | 'price' | 'description', "id">
// type ExcludeId = "name" | "createDate" | "price" | "description"
Pick<Type, Keys>
- 通过从
Type
中选取一组属性Keys
来构造一个类型。
ts
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
Example:
ts
type PickExcludeId = Pick<Product, ExcludeId>
将鼠标放到PickExcludeId
上面显示:
ts
type PickExcludeId = {
name: string;
createDate: string;
price: number;
description: string;
}
Omit<Type, Keys>
- 通过从
Type
中选取所有属性然后移除Keys
来构造类型。
ts
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
Example:
ts
type OmitId = Omit<Product, 'id'>
将鼠标放到OmitId
上面显示:
ts
type OmitId = {
name: string;
createDate: string;
price: number;
description: string;
}
这里 Omit
例子中 的OmitId
和Pick
例子中的PickExcludeId
得到的结果是一致的,从源码也可以看出来Omit
就是Pick
与Exclude
的组合使用。
小红对Partial
和Omit
的学习后恍然大悟,原来她写的
ts
interface OptionalProduct {
name?: string;
createDate?: string,
price?: number;
description?: string;
}
可以这样来写:
ts
type OptionalProduct = Partial<Omit<Product, 'id'>>
OptionalProduct
与Product
进行了强关联,不管Product
增加/减少多少字段,OptionalProduct
都会随之变化。
学到了新知识小红很开心,等下次出现这种需求的时候就了然于胸了,于是修改了代码之后:
ts
type OptionalProduct = Partial<Omit<Product, 'id'>>
const updateProduct = (id: number, productInfo: OptionalProduct) => {
// fetch api update product
}
满意的git add git commit
, review
通过,代码被合入了。但是在第二天小红就收到了一条后端发来的消息: "updateProductApi 接口,前端传参的时候限制一下传给后端的data不能是空的"
也就是说updateProduct('1233', {})
这种数据不能传给后端,小红暗怒,握紧了拳头👊,想找后端去battle,什么f**k后端,真是奇怪的要求,而且这种空对象的情况在后端处理下不就行了? 但是刚入职没几天,现在大环境也不好,还是忍一忍,随即打开编辑器,闷头看起了代码。都不需要怎么考虑,小红便在updateProduct
函数中写了一行判断的代码。
ts
const updateProduct = (id: number, productInfo: RequireAtLeastOne) => {
+ if(Object.keys(productInfo).length === 0) return
// fetch api update product
}
很好,逻辑完全没问题! 等等,productInfo
参数的类型是不是应该也要加个这样的逻辑,限制OptionalProduct
最少要传递一个属性行不行?而且这个属性一定是OptionalProduct
中存在的,这样在类型层面上{}
这种字面量数据传递都不能传递给updateProduct
函数了。
埋头苦思半天无果,小红想到了他们组内有一个有名的Typescript高手小明,于是发了这么一条信息:
于是小红迫不及待的打开了知识库,发现了自己想要的类型RequireAtLeastOne
ts
type RequireAtLeastOne<
ObjectType,
KeysType extends keyof ObjectType = keyof ObjectType,
> = {
// 循环`Key` in `KeysType` 创造出来一个映射类型
[Key in KeysType]-?: Required<Pick<ObjectType, Key>> & // 1. 让 `Key` 的类型必填
// 2. 让其他所有的key 变为可选
Partial<Pick<ObjectType, Exclude<KeysType, Key>>>;
}[KeysType]
打开编辑器进行测试:
ts
const updateProduct = (id: number, productInfo: RequireAtLeastOne<OptionalProduct>) => {
if(Object.keys(productInfo).length === 0) return
// fetch api update product
}
perfect!经过测试后发现完全没问题,小红很开心,但是这个类型她看不太懂于是问了小明。
ts
type RequireAtLeastOne = Required<Pick<OptionalProduct, "name">> | Required<Pick<OptionalProduct, "createDate">> | Required<Pick<OptionalProduct, "price">> | ...
所以用映射类型创建一个新的类型,映射类型的好处就是可以操作传进来的接口类型的key。
ts
type RequireAtLeastOne<
ObjectType,
KeysType extends keyof ObjectType = keyof ObjectType,
> = {
// 循环`Key` in `KeysType` 创造出来一个映射类型
[Key in KeysType]-?: Required<Pick<ObjectType, Key>>
}[KeysType]
然后我们还可能传递进来其他的key,所以要让其他的key变成可选的
ts
Partial<Pick<ObjectType, Exclude<KeysType, Key>>>;
所以最后就变成了
ts
type RequireAtLeastOne<
ObjectType,
KeysType extends keyof ObjectType = keyof ObjectType,
> = {
[Key in KeysType]-?: Required<Pick<ObjectType, Key>> &
Partial<Pick<ObjectType, Exclude<KeysType, Key>>>; // Exclude获取其他的key
}[KeysType]
于是小红开启了一起Typescript之旅。