TypeScript 数据模型层编程的最佳实践

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

虽然 TypeScript 主要用于客户端而数据模型的设计主要是服务端来做的。 但是要写出优雅的代码也还是有不少讲究的。

让我们从一个简单的我的文章列表 api 返回的数据开始返回的文章列表的信息如下

 {"id": 2018,"title" : "TypeScript 数据模型层的编程最佳实践","created" : 1530321232,"last_modified" : 1530320620,"status": 1
} 

同时服务端告诉我们说

status 各值的意思 0/未发布 1/已发布 2/已撤回

最佳实践一 善用枚举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 状态比较多的话根据喜好可以写成下面的这样。

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

考虑到返回的 created 是时间戳值我们还需要添加一个格式化时间戳的函数:

 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 添加几个 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 上时可以看到如下类型提示

所以在开发开始时可以先直接用 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); 

即保证了类型安全使用上又方便代码也不失优雅。

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

“TypeScript 数据模型层编程的最佳实践” 的相关文章

2023的目标,Flag是啥1年前 (2023-02-02)
day031年前 (2023-02-02)
k8s-新增master节点1年前 (2023-02-02)
数组1年前 (2023-02-02)
跟AWS学极致服务1年前 (2023-02-02)