TypeScript 數據模型層編程的最佳實踐

雖然 TypeScript 主要用於客戶端,而數據模型的設計主要是服務端來作的。 可是要寫出優雅的代碼,也仍是有很多講究的。編程

讓咱們從一個簡單的個人文章列表 api 返回的數據開始,返回的文章列表的信息以下:json

{
    "id": 2018,
    "title" : "TypeScript 數據模型層的編程最佳實踐",
    "created" : 1530321232,
    "last_modified" : 1530320620,
    "status": 1
}
複製代碼

同時服務端告訴咱們說:小程序

status 各值的意思 0/未發佈, 1/已發佈, 2/已撤回api

最佳實踐一: 善用枚舉,No Magic constant

對於 status 這種可枚舉的值,爲了不寫出 status === 1 這種跟一個魔法常量的比較的代碼,最佳的作法是寫一個枚舉,並配套一個格式化爲字符串表示的函數,以下:安全

/** * 文章狀態 */
const enum PostStatus {
  /** * 草稿 */
  draft = 0,
  /** * 已發佈 */
  published = 1,

  /** * 已撤回 */
  revoked = 2
}

function formatPostStatus(status: PostStatus) {
  switch (status) {
    case PostStatus.draft:
      return "草稿";
    case PostStatus.published:
      return "已發佈";
    case PostStatus.revoked:
      return "已撤回";
  }
}

複製代碼

若是 PostStatus 狀態比較多的話,根據喜愛能夠寫成下面的這樣。bash

function formatPostStatus(status: PostStatus) {
  const statusTextMap = {
    [PostStatus.draft]: "草稿",
    [PostStatus.published]: "已發佈",
    [PostStatus.revoked]: "已撤回"
  };
  return statusTextMap[status];
}

複製代碼

考慮到返回的 created 是時間戳值,咱們還須要添加一個格式化時間戳的函數:markdown

const enum TimestampFormatterStyle {
  date,
  time,
  datetime
}

function formatTimestamp( timestamp: number, style: TimestampFormatterStyle = TimestampFormatterStyle.date ): string {
  const millis = timestamp * 1000;
  const date = new Date(millis);
  switch (style) {
    case TimestampFormatterStyle.date:
      return date.toLocaleDateString();
    case TimestampFormatterStyle.time:
      return date.toLocaleTimeString();
    case TimestampFormatterStyle.datetime:
      return date.toLocaleString();
  }
}
複製代碼

最佳實踐二:如非必要,不要使用類

上來就搞個數據類

一開始的時候,因爲以前的編程經驗的影響,我一上來就搞一個數據類。以下:數據結構

class Post {
  id: number;
  title: string;
  created: number;
  last_modified: number;
  status: number;

  constructor( id: number, title: string, created: number, last_modified: number, status: number ) {
    this.id = id;
    this.title = title;
    this.created = created;
    this.last_modified = last_modified;
    this.status = status;
  }
}
複製代碼

這可謂分分鐘就寫了 20 行代碼。 而後若是你想到了 TS 提供了簡寫的方式的話,能夠將上面的代碼簡寫以下。函數

class Post {
  constructor( readonly id: number, readonly title: string, readonly created: number, readonly last_modified: number, readonly status: number ) {}
}
複製代碼

也就是說在構造函數中的參數前面添加如 readonly,public,private 等可見性修飾符的話,便可自動建立對應字段。 由於咱們是數據模型,因此咱們選擇使用 readonlypost

通常再在 Post 添加幾個 Getter ,用於返回格式化好的要顯示的屬性值。 以下:

class Post{
 // 構造函數同上
 
 get createdDateString(): string {
    return formatTimestamp(this.created, TimestampFormatterStyle.date);
  }
  
  get lastModifiedDateString(): string {
    return formatTimestamp(this.last_modified, TimestampFormatterStyle.date);
  }

  get statusText(): string {
    return formatPostStatus(this.status);
  }
}
複製代碼

麻煩的開始

好了如今數據類寫好,準備請求數據,綁定數據了。 一開始咱們寫出以下代碼:

const posts:Post[] = resp.data
複製代碼

而後 TS 報以下錯誤:

[ts]
Type '{ id: number; title: string; created: number; last_modifistatic fromJson(json: JsonObject): Post {
    return new Post(
      json.id,
      json.title,
      json.created,
      json.last_modified,
      json.status
    );
  }ed: number; status: number; }[]' is not assignable to type 'Post[]'.
  Type '{ id: number; title: string; created: number; last_modified: number; status: number; }' is not assignable to type 'Post'.
    Property 'createdDateString' is missing in type '{ id: number; title: string; created: number; last_modified: number; status: number; }'.
複製代碼

此時咱們開始意識到,請求回來的jsondata 列表是普通的 object 不能直接給 Post 賦值。 因爲一些編程慣性,咱們開始想着,是否是反序列化一下,將json 對象反序列化成 Post. 因而咱們在 Post 類中添加以下的反序列化方法。

type JsonObject = { [key: string]: any };
class Post{
   // 其餘代碼同上 
   
  static fromJson(json: JsonObject): Post {
    return new Post(
      json.id,
      json.title,
      json.created,
      json.last_modified,
      json.status
    );
  }
}
複製代碼

而後在請求結果處理上增長一過 map 用於反序列化的轉換。以下:

const posts: Post[] = resp.data.map(Post.fromJson);
複製代碼

代碼寫到這裏,思考一下,原來 json 就是一個原生的 JavaScript 對象了。可是咱們又再一步又用來構造出 Post 類。這一步顯得多餘。 另外雖然通常咱們的模型代碼好比 Post 其實能夠根據 api 文檔自動生成, 可是也仍是增長很多代碼。

開始改進

怎麼改進呢? 既然咱們的 json 已是 JavaScrit 對象了,咱們只是缺乏類型聲明。 那咱們直接加上類型聲明的,並且 TS 中的類型聲明,編譯成 js 代碼以後會自動清除的,這樣能夠減小代碼量。這對於小程序開發來講仍是頗有意義的。

天然咱們寫出以下代碼。

interface Post {
  id: number;
  title: string;
  created: number;
  last_modified: number;
  status: number;
}
複製代碼

此時,爲了 UI 模板數據上的綁定。 咱們雙增長了一個叫 PostInfo 的接口。而後將代碼修改以下:

interface PostInfo {
  statusText: string;
  createdDateString: string;
  post: Post;
}

function getPostInfoFromPost(post: Post): PostInfo {
  const statusText = formatPostStatus(post.status);
  const createdDateString = formatTimestamp(post.created);
  return { statusText, createdDateString, post };
}

const postInfos: PostInfo[] = (resp.data as Post[]).map(getPostInfoFromPost);

複製代碼

其實你已知知道貓的樣子

其實我想說的是,咱們上面的代碼中 Post 接口是多餘的。 直接看代碼:

const postDemo = {
  id: 2018,
  title: "TypeScript 數據模型層的編程最佳實踐",
  created: 1530321232,
  last_modified: 1530320620,
  status: 1
};

type Post = typeof postDemo;
複製代碼

當把鼠標放到 Post 上時,能夠看到以下類型提示:

Easy Post interface from

因此在開發開始時,能夠先直接用 API 返回的數據結構看成一個數據模型實例。而後使用 typeof 來獲得對應的類型。

把套去掉

PostInfo 這樣包裝其實挺醜陋的, 由於在咱們內心這裏其實應該是一個 Post 列表,可是爲了格式化一些數據顯示,咱們弄一個 PostInfo 的包裝,這樣在使用上帶來不少不方便。由於當你要使用 Post 的其餘的值時,你總須要多一次間接訪問好比這樣 postInfo.post.id。 這就PostInfo 是咱們在使用 Post 實例時的一個枷鎖,一個套, 如今咱們來將這個套去掉。而去掉這個套的方法使用了兩項技術。 一個是 TS 中接口的繼承,一個是 Object.assign 這個方法。 直接用代碼說話:

interface PostEx extends Post {
  statusText: string;
  createdDateString: string;
}

function getPostExFromPost(post: Post): PostEx {
  const statusText = formatPostStatus(post.status);
  const createdDateString = formatTimestamp(post.created);
  return Object.assign(post, { statusText, createdDateString });
}

const posts: PostEx[] = (resp.data as Post[]).map(getPostExFromPost);

複製代碼

即保證了類型安全,使用上又方便,代碼也不失優雅。

相關文章
相關標籤/搜索