最近半年陆续交接了几位同事的代码,发现虽然用了严格的eslint来规范代码的书写方式,同时项目也全量使用了Typescript,可是在review代码的过程当中,仍是有不少不整洁不规范的地方。良好的代码具备很好的可读性,后续维护起来也会使人愉悦,也能下降重构的几率。本文会结合Typescript,谈谈如何clean代码:node
- 基础规范
- 函数式
常量必须命名, 在作逻辑判断的时候,也不容许直接对比没有命名的常量。es6
switch(num){
case 1:
...
case 3:
...
case 7:
...
}
if(x === 0){
...
}
复制代码
上述的例子中,根本不知道1 3 7 对应的是什么意思,这种写法就基本上没有可读性。typescript
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等逻辑判断的时候,咱们能够从变量名得知常量的具体含义,增长了可读性。编程
除了常量枚举外,在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对象的。api
正确的写法数组
enum FruitEnum {
tomato = 1,
banana = 2,
apple = 3
}
const FruitList = Object.entries(FruitEnum)
复制代码
上述就是正确的写法,这种写法不只仅是不冗余,此外,若是修改了枚举的类型,咱们只要直接修改枚举,这样衍生的数组也会改变。安全
除此以外,字符串枚举值和字符串是有本质区别的,在定义类型的时候请千万注意,要否则会让你写的代码很冗余。markdown
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的断言。app
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。
Typescript中应该严格禁止使用ts-ignore,ts-ignore是一个比any更加影响Typescript代码质量的因素。对于any,在个人项目中曾一度想把any也禁掉,可是有一些场景中是须要使用any的,所以没有粗鲁的禁止any的使用。可是绝大部分场景下,你可能都不须要使用any.须要使用any的场景,能够case by case的分析。
//@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须要在使用前,强制判断其类型。
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?
复制代码
export class Triangle {
/* ... */
}
export class Square {
/* ... */
}
复制代码
上述直接使用module,就是正确的方法,在模块系统中自己就能够避免变量命名重复,所以namespace是没有意义的。
在定义函数的时候,应该减小函数参数的个数,推荐不能超过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
}) //正确的写法
复制代码
请不要使用模块的反作用。要保证模块的使用应该是先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();
复制代码
非空断言自己是不安全的,主观的判断存在偏差,从防护性编程的角度,是不推荐使用非空断言的。
let x:string|undefined = undefined
x!.toString()
复制代码
由于使用了非空断言,所以编译的时候不会报错,可是运行的时候会报错.
比较推荐使用的是optional chaining。以?.的形式。
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 ISpiderMan = 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等等均可能存在同名类型合并,在业务代码中我的不推荐使用类型合并。
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 {
}
}
复制代码
在项目中合理地使用自定义守卫,能够帮助咱们减小不少没必要要的类型断言,同时改善代码的可读性。
不论是变量名仍是函数名,请千万不要使用非命名,在业务中我就遇到过这个问题,后端定义了一个非命名形式的变量isNotRefresh:
let isNotRefresh = false //是否不刷新,否表示刷新
复制代码
isNotRefresh表示不刷新,这样定义的变量会致使跟这个变量相关的不少逻辑都是相反的。正确的形式应该是定义变量是isRefresh表示是否刷新。
let isRefresh = false //是否刷新,是表示刷新
复制代码
我的很是推荐函数式编程,主观的认为链式调用优于回调,函数式的方式又优于链式调用。近年来,函数式编程日益流行,Ramdajs、RxJS、cycleJS、lodashJS等多种开源库都使用了函数式的特性。本文主要介绍一下如何使用ramdajs来简化代码。
我的认为函数声明式的调用比命令式更加简洁,举例来讲:
//命令式
let names:string[] = []
for(let i=0;i<persons.length;i++){
names.push(person[i].name)
}
//声明式
let names = persons.map((item)=>item.name)
复制代码
从上述例子咱们能够看出来,明显函数调用声明式的方法更加简洁。此外对于没有反作用的函数,好比上述的map函数,彻底能够不考虑函数内部是如何实现的,专一于编写业务代码。优化代码时,目光只须要集中在这些稳定坚固的函数内部便可。
推荐使用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){
person = 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);
复制代码