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

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

 {    "id": 2018,    " " : "Type  数据模型层的编程最佳实践",    "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;   : string;  created: number;  last_modified: number;  status: number;  constructor(    id: number,     : string,    created: number,    last_modified: number,    status: number  ) {    this.id = id;    this.  =  ;    this.created = created;    this.last_modified = last_modified;    this.status = status;  }}复制代码

这可谓分分钟就写了 20 行代码。 然后如果你想到了 TS 提供了简写的方式的话,可以将上面的代码简写如下。

class Post {  constructor(    readonly id: number,    readonly  : 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;  : string; created: number; last_modifistatic fromJson(json: Json ): Post {    return new Post(      json.id,      json. ,      json.created,      json.last_modified,      json.status    );  }ed: number; status: number; }[]' is not assignable to type 'Post[]'.  Type '{ id: number;  : string; created: number; last_modified: number; status: number; }' is not assignable to type 'Post'.    Property 'createdDateString' is missing in type '{ id: number;  : string; created: number; last_modified: number; status: number; }'.复制代码

此时我们开始意识到,请求回来的jsondata 列表是普通的 不能直接给 Post 赋值。 由于一些编程惯性,我们开始想着,是不是反序列化一下,将json 对象反序列化成 Post. 于是我们在 Post 类中添加如下的反序列化方法。

type Json  = { [key: string]: any };class Post{   // 其他代码同上      static fromJson(json: Json ): Post {    return new Post(      json.id,      json. ,      json.created,      json.last_modified,      json.status    );  }}复制代码

然后在请求结果处理上增加一过 map 用于反序列化的转换。如下:

const posts: Post[] = resp.data.map(Post.fromJson);复制代码

代码写到这里,思考一下,原来 json 就是一个原生的 对象了。但是我们又再一步又用来构造出 Post 类。这一步显得多余。另外虽然一般我们的模型代码比如 Post 其实可以根据 api 文档自动生成,但是也还是增加不少代码。

开始改进

怎么改进呢? 既然我们的 json 已经是 JavaScrit 对象了,我们只是缺少类型声明。 那我们直接加上类型声明的,而且 TS 中的类型声明,编译成 js 代码之后会自动清除的,这样可以减少代码量。这对于小程序开发来说还是很有意义的。

自然我们写出如下代码。

interface Post {  id: number;   : 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,   : "Type  数据模型层的编程最佳实践",  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 中接口的继承,一个是 .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  .assign(post, { statusText, createdDateString });}const posts: PostEx[] = (resp.data as Post[]).map(getPostExFromPost);复制代码

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


作者:banxi
原文发布时间:2018年06月30日
本文来源掘金如需转载请紧急联系作者
收藏 打印