小红的Typescript之旅【入职】

故事是这样的, 小红最近入职了一家新公司,摸鱼没几天主管就分了个需求,工作量不大,于是小红开始熟练的crud了的起来。

​ 不太爽的是项目里开启了noImplicitAny ,anyscript大法失效了,小红心里也明白,一直any下去也不是个事,等后面在好好系统的学习下Typescript,可是在这次联调接口的时候却出现了一个类型的问题,来看小红是怎么解决的吧。

这次的需求是对商品界面进行增删改查,小红根据后端接口字段熟练的定义起了Product类型,然后根据业务进行编码

ts 复制代码
interface Product {
  id: string;
  name: string;
  createDate: string,
  price: number;
  description: string;
  // ...
}

// code something...

联调接口的时候,后台采用的是restful风格的接口,在定义修改函数的时候,小红给参数idproductInfo分别标注了stringProduct类型。

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提供的PartialOmit类型来重新构造OptionalProduct, 不会的再来问我"。

于是小红根据提示的信息开始了对PartialOmit的学习:

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例子中 的OmitIdPick例子中的PickExcludeId得到的结果是一致的,从源码也可以看出来Omit就是PickExclude的组合使用。

小红对PartialOmit的学习后恍然大悟,原来她写的

ts 复制代码
interface OptionalProduct {
  name?: string;
  createDate?: string,
  price?: number;
  description?: string;
}

可以这样来写:

ts 复制代码
type OptionalProduct = Partial<Omit<Product, 'id'>>

OptionalProductProduct进行了强关联,不管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之旅。

相关推荐
长天一色7 分钟前
【ECMAScript 从入门到进阶教程】第三部分:高级主题(高级函数与范式,元编程,正则表达式,性能优化)
服务器·开发语言·前端·javascript·性能优化·ecmascript
NiNg_1_23424 分钟前
npm、yarn、pnpm之间的区别
前端·npm·node.js
秋殇与星河27 分钟前
CSS总结
前端·css
BigYe程普1 小时前
我开发了一个出海全栈SaaS工具,还写了一套全栈开发教程
开发语言·前端·chrome·chatgpt·reactjs·个人开发
余生H1 小时前
前端的全栈混合之路Meteor篇:关于前后端分离及与各框架的对比
前端·javascript·node.js·全栈
程序员-珍1 小时前
使用openapi生成前端请求文件报错 ‘Token “Integer“ does not exist.‘
java·前端·spring boot·后端·restful·个人开发
axihaihai1 小时前
网站开发的发展(后端路由/前后端分离/前端路由)
前端
流烟默1 小时前
Vue中watch监听属性的一些应用总结
前端·javascript·vue.js·watch
2401_857297912 小时前
招联金融2025校招内推
java·前端·算法·金融·求职招聘
茶卡盐佑星_2 小时前
meta标签作用/SEO优化
前端·javascript·html