最近半年陸續交接了幾位同事的代碼,發現雖然用了嚴格的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);
複製代碼