預設值+TS型別約束提高資料處理成功率

邊城發表於2020-06-28

我們在處理資料時,常常會遇到某項資料,或者屬性是 undefined,從而引起中斷性錯誤,造成資料處理失敗。解決這一問題最直接的辦法就是在使用前判斷是否 undefined ,但是如果每一個資料使用前都進行判斷,非常繁瑣,而且容易遺漏。所以這裡給大家介紹兩個辦法:

  • 利用預設值解決 undefined,避免繁瑣的判斷過程;
  • 利用 TypeScript 的型別系統對資料進行檢查,避免遺漏。

為了更直觀的描述問題和解決辦法,我們設計了這樣一個需求:

需求:後端響應資料轉換成 UI 所需要的資料

一般來說,現在的前端元件對錶格資料的要求,都是:

  • 以陣列表示資料行,即當前頁資料
  • 每行是一個資料物件,其屬性對應於表格列
  • 除資料行之外,還包含一些附加資訊用於分頁,比如

    • 庫存資料總數(不是當前資料行的總數)
    • 每頁資料呈現條數(一般由前端傳給後端,後端返回資料時帶出來)
    • 當前頁數(一般由前端傳給後端,後端返回資料時帶出來)

那麼,從後端返回的資料可能會是這樣(JSON 示例)

{
  "code": 0,
  "message": "查詢成功",
  "data": {
    "total": 12345,
    "page": 3,
    "rows": [
      {
        "id": 34,
        "title": "第 34 號資料",
        "stamp": "2020-06-25T20:18:19Z"
      },
      ...
    ]
  }
}

前端呈現如果使用 Layui 的資料表格,它所要求的資料格式是這樣的(來自官方文件):

{
  "code": 0,
  "msg": "",
  "count": 1000,
  "data": [{}, {}]
} 

遠端響應資料結構和表格需要的資料結構之間,對應關係非常明顯。

我們用 res 來表示後端 JSON 轉成的 JavaScript 物件,那麼為 Layui 準備的資料會這樣取值:

const tableData = {
    code: res.code,
    msg: res.message,
    count: res.data.total,
    data: res.data.rows
}

這個處理轉換非常簡單,只是做了一個屬性名稱的變化。

但是遠端返回的資料,可能沒有 code,或者 message,甚至沒有 data,那麼上面的處理結果就可能包括 undefined。前端元件不希望有這樣的值,所以需要新增預設值處理。

利用解構變數可以賦予預設值這一特性

為了演示預設值處理,我們假設,後端返回的資料規範比較靈活,為了節約網路資源,有一個預設值約定:

  • 如果請求正常完成,省略 "code": 0
  • 如果沒有特殊訊息,省略 "message": ""
  • 如果沒有資料,即 "total": 0 的情況,省略 "data": {}
  • 如果當前頁沒有資料,省略 "rows": []

這種情況下,在進行資料轉換時就需要充分考慮到某項資料可能不存在,避免 undefined 錯誤。

Object.assign() 或者 Spread 運算子可以部分解決這個問題,但是

  • 只能解決單層(直接)屬性,不能解決深層屬性預設值
  • 有坑,它們對 “undefiend 屬性”和“不存在的屬性”處理行為不同

不過我們可以利用解構變數能夠賦予預設值的特性來進行處理。下面這段程式碼就巧妙地利用了這一特性來避免 undefined 錯誤。

function getData(res) {
    const { code = 0, message: msg, data = {} } = res;
    const { total: count = 0, rows = [] } = data;

    return {
        code,
        msg,
        count,
        data: rows
    }
}

const tableData = getData(res);

解構確實是可以解決問題,但是如果遺漏或者寫錯屬性,除錯起來恐怕不易。比如上面

const { message: msg } = res;

就很容易錯寫成

const { msg } = res

這是 JS 的硬傷,即使用 ESLint 這樣強悍的靜態檢查工具也不能解決。但是如果引入強型別定義,使用 TypeScript 就好辦了。

使用 TypeScript 和型別檢查轉換過程

既然用 TypeScript,就需要為兩個資料結構定義對應的型別了。

我們有兩個資料結構 restableData,在 TypeScript 裡可以直接把它們定義為 any 型別,這是最簡單的操作,但是沒什麼用 —— 因為 TypeScript 不檢查 any 型別。

所以先根據我們之前的約定,把 restableData 的型別定義出來:

interface FetchData<T> {
    total: number;
    page?: number;
    rows?: T[];
}

interface FetchResult<T> {
    code?: number;
    message?: string;
    data?: FetchData<T>;
}

interface LayuiTalbeData<T> {
    code: number;
    msg?: string;
    count: number;
    data: T[];
}

然後要把 res 宣告成 FetchResult<T> 型別。這裡我們暫時不關心具體每行資料的結構,所以直接用 any 代替好了

const res: FetchResult<any> = await fetch();
// 或者
// const res = await fetch() as FetchResult<any>;

這種情況下,假如我們不小心寫錯了屬性名,比如解構時把源屬性 message 錯寫成了目錄屬性名 msg,即 const { msg } = res,VSCode 是會提示錯誤的:

image.png

解決的辦法是使用解構重新命名:

const { message: msg } = res;

或者,如果我們忘了處理 undefined,比如忘了給解構的 rows 賦予初始值,那也會得到錯誤提示,因為

  • 源資料定義中 rows?: T[] 表示它可省略,即可能是 undefined
  • 目標資料定義中 data: T[] 表示它一定不會是 undefined

image.png

解決的辦法是,賦予初始值,使其不可能為 undefined

const { rows: [] } = data;

完整的 TypeScript 程式碼如下(型別定義參考前面的定義):

function getData<T>(res: FetchResult<T>): LayuiTalbeData<T> {
    const { code = 0, message: msg, data = {} as FetchData<T> } = res;
    const { total: count = 0, rows = [] } = data;

    return {
        code,
        msg,
        count,
        data: rows
    }
}

// 這裡 TypeScript 可以推匯出 tableData 型別是 LayuiTalbeData<any>
const tableData = getData(res);

使用 Optional Chain 和 Nullish Coalescing

回到 JavaScript,其實還有一個辦法可以處理預設值的問題:

const tableData = {
    code: res.code || 0,
    msg: res.message || "",
    count: (res.data && res.data.total) || 0,
    data: (res.data && res.data.rows) || []
}

這也是一個非常常見的辦法。這個辦法在 TypeScript 中配置型別定義同樣可行。只是對多層屬性的處理仍然顯得有點麻煩。不過 JavaScript 最近引入了“Optional Chain”和“Nullish Coalescing”特性,這個程式碼可以更簡潔:

const tableData = {
    code: res.code ?? 0,
    msg: res.message,
    count: res.data?.total ?? 0,
    data: res.data?.rows ?? []
};

放在 TypeScript 中是這樣寫的:

const tableData: LayuiTalbeData<any> = {
    code: res.code ?? 0,
    msg: res.message,
    count: res.data?.total ?? 0,
    data: res.data?.rows ?? []
}

上面的 TypeScript 程式碼中,如果錯寫了 res.msg 或者忘了加 ?? [] 等,TSC 或者 VSCode 都會有錯誤提示,以保證你能修正程式碼。

看,新特性配合 TypeScript 的強型別檢查,簡直是完美!

小結

我們講了最簡單的資料轉換:直接按源資料屬性取值。雖然簡單,但是有坑,也有處理的方法和技巧:

  • 注意可能出現的 undefinednull,甚至 NaN 等需要特殊處理的資料
  • 使用解構將屬性提取出來,並根據資料結構的需要適當賦予初始值
  • 使用 Optional Chain 和 Nullish Coalescing 簡化對可能為 undefined 屬性的處理
  • 使用 TypeScript 在開發期檢查錯誤

這裡講的資料處理比較基礎,但其中的坑也比較容易被忽略。後面在專欄或訂閱號中,我們還會繼續探討更復雜一些的資料處理分析方法和處理技巧,請關注!

相關文章