您现在的位置是:亿华云 > 系统运维
Typescript代码整洁之道
亿华云2025-10-04 03:36:05【系统运维】4人已围观
简介最近半年陆续交接了几位同事的代码,发现虽然用了严格的eslint来规范代码的书写方式,同时项目也全量使用了Typescript,但是在review代码的过程中,还是有很多不整洁不规范的地方。良好的代码
最近半年陆续交接了几位同事的代码代码,发现虽然用了严格的整洁之道eslint来规范代码的书写方式,同时项目也全量使用了Typescript,代码但是整洁之道在review代码的过程中,还是代码有很多不整洁不规范的地方。良好的整洁之道代码具有很好的可读性,后续维护起来也会令人愉悦,代码也能降低重构的整洁之道概率。本文会结合Typescript,代码谈谈如何clean代码:
基础规范 函数式一、整洁之道基础规范
(1)常量
常量必须命名,代码 在做逻辑判断的时候,也不允许直接对比没有命名的整洁之道常量。
错误的代码书写 switch(num){ case 1: ... case 3: ... case 7: ... } if(x === 0){ ... }上述的例子中,根本不知道1 3 7 对应的整洁之道是什么意思,这种写法就基本上没有可读性。代码
正确的写法 enum DayEnum { oneDay = 1, threeDay = 3, oneWeek = 7, } let num = 1; switch(num){ case DayEnum.oneDay: ... case DayEnum.threeDay: ... case DayEnum.oneWeek: ... } const RightCode = 0; if(x === RightCode)从上述正确的写法可以看出来,网站模板常量有了命名,在switch或者if等逻辑判断的时候,我们可以从变量名得知常量的具体含义,增加了可读性。
(2)枚举
除了常量枚举外,在Typescript的编译阶段,枚举会生成一个maping对象,如果不是字符串枚举,甚至会生成一个双向的mapping。因此在我们的业务代码中,有了枚举,就不需要一个与枚举值相关的数组。
错误的写法 enum FruitEnum { tomato = 1, banana = 2, apple = 3 } const FruitList = [ { key:1, value: tomato },{ key:2, value: banana },{ key:3, value: apple } ]这里错误的原因是冗余,我们要得到一个FruitList,并不需要new一个,而是可以直接根据FruitEnum的枚举来生成一个数组,原理就是我们之前所说的Typescript的枚举,除了常量枚举外,香港云服务器在编译的时候是会生成一个map对象的。
正确的写法 enum FruitEnum { tomato = 1, banana = 2, apple = 3 } const FruitList = Object.entries(FruitEnum)上述就是正确的写法,这种写法不仅仅是不冗余,此外,如果修改了枚举的类型,我们只要直接修改枚举,这样衍生的数组也会改变。
除此之外,字符串枚举值和字符串是有本质区别的,在定义类型的时候请千万注意,要不然会让你写的代码很冗余。
错误的用法 enum GenderEnum{ male = 男生, female = 女生 } interface IPerson{ name:string gender:string } let bob:IPerson = { name:"bob",gender:male} <span>{ Gender[bob.gender as keyof typeof GenderEnum]}</span>上述的错误的原因就是IPerson的类型定义中,gender不应该是string,而应该是服务器托管一个枚举的key,因此,在将string转枚举值的时候,必须增加一个as keyof typeof GenderEnum的断言。
正确的写法 enum GenderEnum{ male = 男生, female = 女生 } interface IPerson{ name:string gender:keyof typeof GenderEnum } let bob:IPerson = { name:"bob",gender:male} <span>{ Gender[bob.gender]}</span>上述 就是正确的写法,字符串枚举和字符串类型是有 明显区别的,当某个变量需要使用到枚举时,不能将他定义成string。
(3)ts-ignore & any
Typescript中应该严格禁止使用ts-ignore,ts-ignore是一个比any更加影响Typescript代码质量的因素。对于any,在我的项目中曾一度想把any也禁掉,但是有一些场景中是需要使用any的,因此没有粗鲁的禁止any的使用。但是绝大部分场景下,你可能都不需要使用any.需要使用any的场景,可以case by case的分析。
错误使用ts-ignore的场景 //@ts-ignore import Plugin from someModule //如果someModule的声明不存在 Plugin.test("hello world")上述就是最经典的使用ts-ignore的场景,如上的方式使用了ts-ignore.那么Typescript会认为Plugin的类型是any。正确的方法通过declare module的方法自定义需要使用到的类型.
正确的方法 import Plugin from someModule declare module someModule { export type test = (arg: string) => void; }在module内部可以定义声明,同名的声明遵循一定 的合并原则,如果要扩展三方模块,declare module是很方便的。
同样的大部分场景下,你也不需要使用any,部分场景下如果无法立刻确定某个值的类型,我们可以 用unknown来代替使用any。
any会完全失去类型判断,本身其实是比较危险的,且使用any就相当于放弃了类型检测,也就基本上放弃了typescript。举例来说:
let fish:any = { type:animal, swim:()=> { } } fish.run()上述的例子中我们调用了一个不存在的方法 ,因为使用了any,因此跳过了静态类型检测,因此是不安全的。运行时会出错,如果无法立刻确定某个值的类型,我们可以 用unknown来代替使用any。
let fish:unknown = { type:animal, swim:()=> { } } fish.run() //会报错unkonwn是任何类型的子类型,因此跟any一样,任意类型都可以赋值给unkonwn。与any不同的是,unkonwn的变量必须明确自己的类型,类型收缩或者类型断言后,unkonwn的变量才可以正常使用其上定义的方法和变量。
简单来说,unkonwn需要在使用前,强制判断其类型。
(4)namespace
Typescript的代码中,特别是偏业务的开发中,你基本上是用不到namespace的。此外module在nodejs中天然支持,此外在es6(next)中 es module也成为了一个语言级的规范,因此Typescript官方也是推荐使用module。
namespace简单来说就是一个全局对象,当然我们也可以把namespace放在module中,但是namespace放在module中也是有问题的。
错误的方法 //在一个shapes.ts的模块中使用 export namespace Shapes { export class Triangle { /* ... */ } export class Square { /* ... */ } } //我们使用shapes.ts的时候 //shapeConsumer.ts import * as shapes from "./shapes"; let t = new shapes.Shapes.Triangle(); // shapes.Shapes? 正确的方法(直接使用module) export class Triangle { /* ... */ } export class Square { /* ... */ }上述直接使用module,就是正确的方法,在模块系统中本身就可以避免变量命名重复,因此namespace是没有意义的。
(5)限制函数参数的个数
在定义函数的时候,应该减少函数参数的个数,推荐不能超过3个。
错误的用法 function getList(searchName:string,pageNum:number,pageSize:number,key1:string,key2:string){ ... }不推荐函数的参数超过3个,当超过3个的时候,应该使用对象来聚合。
正确的用法 interface ISearchParams{ searchName:string; pageNum:number; pageSize:number; key1:string; key2:string; } function getList(params:ISearchParams){ }同样的引申到React项目中,useState也是同理
const [searchKey,setSearchKey] = useState(); const [current,setCurrent] = useState(1) const [pageSize,setPageSize] = useState(10) //错误的写法 const [searchParams,setSearchParams] = useState({ searchKey: , current:1, pageSize:10 }) //正确的写法(6)module模块尽量保证无副作用
请不要使用模块的副作用。要保证模块的使用应该是先import再使用。
错误的方法 //Test.ts window.x = 1; class Test{ } let test = new Test() //index.ts import from ./test ...上述在index.ts中import的模块,其调用是在test.ts文件内部的,这种方法就是import了一个有副作用的模块。
正确的方法应该是保证模块非export变量的纯净,且调用方在使用模块的时候要先import,后调用。
正确的方法 //test.ts class Test{ constructor(){ window.x = 1 } } export default Test //index.ts import Test from ./test const t = new Test();(7)禁止使用!.非空断言
非空断言本身是不安全的,主观的判断存在误差,从防御性编程的角度,是不推荐使用非空断言的。
错误的用法 let x:string|undefinedundefined = undefined x!.toString()因为使用了非空断言,因此编译的时候不会报错,但是运行的时候会报错.
比较推荐使用的是optional chaining。以?.的形式。
(8)使用typescript的内置函数
typescript的很多内置函数都可以复用一些定义。这里不会一一介绍,常见的有Partial、Pick、Omit、Record、extends、infer等等,如果需要在已有的类型上,衍生出新的类型,那么使用内置函数是简单和方便的。此外还可以使用 联合类型、交叉类型和类型合并。
联合类型 //基本类型 let x:number|string x= 1; x = "1" //多字面量类型 let type:primary|danger|warning|error = primary值得注意的是字面量的赋值。
let type:primary|danger|warning|error = primary let test = error type = test //报错 let test = error as const type = test //正确 交叉类型 interface ISpider{ type:string swim:()=>void } interface IMan{ name:string; age:number; } type ISpiderISpiderMan = ISpider & IMan let bob:ISpiderMan = { type:"11",swim:()=>{ },name:"123",age:10} 类型合并最后讲一讲类型合并,这是一种极其不推荐的方法。在业务代码中,不推荐使用类型合并,这样会增加代码的阅读复杂度。类型合并存在很多地方。class、interface、namespace等之间都可以进行类型合并,以interface为例:
interface Box { height: number; width: number; } interface Box { scale: number; } let box: Box = { height: 5, width: 6, scale: 10 };上述同名的interface Box是会发生类型合并的。不仅interface和 interface可以类型合并,class和interface,class和namesppace等等都可能存在同名类型合并,在业务代码中个人不推荐使用类型合并。
(9)封装条件语句以及ts的类型守卫
错误的写法 if (fsm.state === fetching && isEmpty(listNode)) { // ... } 正确的写法 function shouldShowSpinner(fsm, listNode) { return fsm.state === fetching && isEmpty(listNode); } if (shouldShowSpinner(fsmInstance, listNodeInstance)) { // ... }在正确的写法中我们封装了条件判断的逻辑成一个独立函数。这种写法比较可读,我们从函数名就能知道做了一个什么判断。
此外封装条件语句也可以跟ts的自定义类型守卫挂钩。来看一个最简单的封装条件语句的自定义类型守卫。
function IsString (input: any): input is string { return typeof input === string; } function foo (input: string | number) { if (IsString(input)) { input.toString() //被判断为string } else { } }在项目中合理地使用自定义守卫,可以帮助我们减少很多不必要的类型断言,同时改善代码的可读性。
(10)不要使用非变量
不管是变量名还是函数名,请千万不要使用非命名,在业务中我就遇到过这个问题,后端定义了一个非命名形式的变量isNotRefresh:
let isNotRefresh = false //是否不刷新,否表示刷新isNotRefresh表示不刷新,这样定义的变量会导致跟这个变量相关的很多逻辑都是相反的。正确的形式应该是定义变量是isRefresh表示是否刷新。
let isRefresh = false //是否刷新,是表示刷新二、函数式
个人非常推荐函数式编程,主观的认为链式调用优于回调,函数式的方式又优于链式调用。近年来,函数式编程日益流行,Ramdajs、RxJS、cycleJS、lodashJS等多种开源库都使用了函数式的特性。本文主要介绍一下如何使用ramdajs来简化代码。
(1)声明式和命令式
个人认为函数声明式的调用比命令式更加简洁,举例来说:
//命令式 let names:string[] = [] for(let i=0;i<persons.length;i++){ names.push(person[i].name) } //声明式 let names = persons.map((item)=>item.name)从上述例子我们可以看出来,明显函数调用声明式的方法更加简洁。此外对于没有副作用的函数,比如上述的map函数,完全可以不考虑函数内部是如何实现的,专注于编写业务代码。优化代码时,目光只需要集中在这些稳定坚固的函数内部即可。
(2)Ramdajs
推荐使用ramdajs,ramdajs是一款优秀的函数式编程库,与其他函数式编程库相比较,ramdajs是自动柯里化的,且ramdajs提供的函数从不改变用户已有数据。
来自最近业务代码中的一个简单的例子:
/** * 获取标签列表 */ const getList = async () => { pipeWithP([ () => setLoading(true), async () => request.get(, { params: { action: API.getList}, }), async (res: IServerRes) => { R.ifElse( R.isEqual(res.message === success), () => setList(res.response.list); )(); }, () => setLoading(false) ])(); };上述是业务代码中的一个例子,利用pipe可以使得流程的操作较为清晰,此外也不用定义中间变量。
再来看一个例子:
let persons = [ { username: bob, age: 30, tags: [work, boring]}, { username: jim, age: 25, tags: [home, fun]}, { username: jane, age: 30, tags: [vacation, fun]} ]我们需要从这个数组中找出tags包含fun的对象。如果用命令式:
let NAME = fun let person; for(let i=0;i<persons.length;i++){ let isFind = false let arr = persons[i].tags; for(let j = 0;j<arr.length;j++){ if(arr[i] === NAME){ isFind = true break; } } if(isFind){ personperson = person[i] break; } }我们用函数式的写法可以简化:
let person = R.filter(R.where({ tags: R.includes(fun)}))很明显减少了代码量且更加容易理解含义。
最后再来看一个例子:
const oldArr= [[[[[{ name: yuxiaoliang}]]]]];我们想把oldArr这个多维数组,最内层的那个name,由小写转成大写,用函数式可以直接这样写。
R.map(atem => R.map(btem => R.map(ctem => R.map(dtem => R.map(etem => etem.name.toUpperCase())(dtem))(ctem))(btem))(atem), )(arr);很赞哦!(2)
相关文章
- 在数以亿计的网站中,我们应该抓住每一个可能带来宣传的机会,域名可以带有企业的名字,一般可以使用汉语拼音或者英语单词或者是相关缩写的形式,只要用户记住了你企业的名字,就能很容易的打出你的网站域名,同样的,记住了网站域名也能很快的记住你公司的名字。
- 在 Linux 上使用 jps 命令检查 Java 进程
- 一篇学会如何使用 Myloader 恢复数据
- 面试官:说下你对方法区演变过程和内部结构的理解
- 公司和个人选域名方法一样吗?有什么不同?
- 使用Python开发鸿蒙设备程序(2-I2C应用实例)
- 三分钟图解事务隔离级别,看一遍就懂
- 使用 Grpcurl 通过命令行访问 gRPC 服务
- 在众多公司中,如果我们必须选择一家可信的公司,那当然是信得过的。
- Feign的一个注解居然隐藏这么多知识!