雖然 TypeScript 主要用於客戶端,而數據模型的設計主要是服務端來作的。 可是要寫出優雅的代碼,也仍是有很多講究的。編程
讓咱們從一個簡單的個人文章列表 api 返回的數據開始,返回的文章列表的信息以下:json
{ "id": 2018, "title" : "TypeScript 數據模型層的編程最佳實踐", "created" : 1530321232, "last_modified" : 1530320620, "status": 1 } 複製代碼
同時服務端告訴咱們說:小程序
status 各值的意思 0/未發佈, 1/已發佈, 2/已撤回api
對於 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
等可見性修飾符的話,便可自動建立對應字段。 由於咱們是數據模型,因此咱們選擇使用 readonly
。post
通常再在 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; }'.
複製代碼
此時咱們開始意識到,請求回來的json
的 data
列表是普通的 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
上時,能夠看到以下類型提示:
因此在開發開始時,能夠先直接用 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); 複製代碼
即保證了類型安全,使用上又方便,代碼也不失優雅。