Typescript代碼整潔之道


    最近半年陸續交接了幾位同事的代碼,發現雖然用了嚴格的eslint來規範代碼的書寫方式,同時項目也全量使用了Typescript,可是在review代碼的過程當中,仍是有不少不整潔不規範的地方。良好的代碼具備很好的可讀性,後續維護起來也會使人愉悅,也能下降重構的機率。本文會結合Typescript,談談如何clean代碼:node

  • 基礎規範
  • 函數式

1、基礎規範

(1)常量

     常量必須命名, 在作邏輯判斷的時候,也不容許直接對比沒有命名的常量。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等邏輯判斷的時候,咱們能夠從變量名得知常量的具體含義,增長了可讀性。編程

(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對象的。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

(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|undefined = 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 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等等均可能存在同名類型合併,在業務代碼中我的不推薦使用類型合併。

(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  //是否刷新,是表示刷新

複製代碼

2、函數式

    我的很是推薦函數式編程,主觀的認爲鏈式調用優於回調,函數式的方式又優於鏈式調用。近年來,函數式編程日益流行,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){
      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);

複製代碼
相關文章
相關標籤/搜索