changer

TypeScript: 使用JSON

原文链接: choly.ca

编辑:

  • 在Date上调用toString是为了便于说明。

  • 最后有一个完整的注释示例。

  • 按照建议使用toJSON方法 Schipperz.

  • 按照建议添加reviver方法 Anders Ringqvist.


代码中有一个User类型。

interface User {
  name:    string;
  age:     number;
  created: Date;
}


在某种程度上,您可能希望将其编码为JSON。工作原理正如您所期望的那样。

 > JSON.stringify({ name: "bob", age: 34, created: new Date() });
'{"name":"bob","age":34,"created":"2016-03-19T18:15:12.710Z"}'


问题是,当您解析创建的字段时,它不再是一个日期。

> JSON.parse('{"name":"bob","age":34,"created":"2016-03-19T18:15:12.710Z"}')
{ name: 'bob', age: 34, created: '2016-03-19T18:15:12.710Z' }


我解决这个问题的方法是引入一个UserJSON接口。由于它只包含基本类型,因此可以在不更改JSON的情况下对其进行转换。

interface UserJSON {
  name:    string;
  age:     number;
  created: string;
}


然后我将User -> UserJSON在“字符串化”之前转换为JSON,并将UserJSON ->User在解析后转换为JSON。下面是一些代码执行此操作的示例。

function getUsers(): Promise<User[]> {
  return ajax.get<UserJSON[]>('/users').then(data => {
    return data.data.map(decodeUser);
  });
}

function updateUser(id: number|string, user: User): Promise<{}> {
  return ajax.put<{}>(/users/${id}, encodeUser(user));
}


这里是一些转发方法

function encodeUser(user: User): UserJSON {
  return {
    name:    user.name,
    age:     user.age,
    created: user.created.toString()
  };
}

function decodeUser(json: UserJSON): User {
  return {
    name:    json.name,
    age:     json.age,
    created: new Date(json.created)
  };
}


这是可行的,但这是一个人为的例子。在实际情况中,会有更多的属性,这很快就会变成一个非常麻烦的问题。用se Object.assign 把它清理一下。

function encodeUser(user: User): UserJSON {
  return Object.assign({}, user, {
    created: user.created.toString()
  });
}

function decodeUser(json: UserJSON): User {
  return Object.assign({}, json, {
    created: new Date(json.created)
  });
}


到目前为止还不错,但是当User是一个类时会发生什么呢?

class User {

  private created: Date;

  constructor(
    private name: string,
    private age:  string
  ) {
    this.created = new Date();
  }

  getName(): string {
    return this.name;
  }
}


为了让它工作,我使用 Object.create 在不使用构造函数的情况下创建User的新实例。然后给它分配属性,编码函数不变。

function decodeUser(json: UserJSON): User {
  let user = Object.create(User.prototype);
  return Object.assign(user, json, {
    created: new Date(json.created)
  });
}


最后,encode和decode函数可以只是User类上的方法。

class User {

  private created: Date;

  constructor(
    private name: string,
    private age:  string
  ) {
    this.created = new Date();
  }

  getName(): string {
    return this.name;
  }

  encode(): UserJSON {
    return Object.assign({}, this, {
      created: this.created.toString()
    });
  }

  static decode(json: UserJSON): User {
    let user = Object.create(User.prototype);
    return Object.assign(user, json, {
      created: new Date(json.created)
    });
  }
}


当JSON.stringify在对象上调用,它检查一个名为toJSON的方法,以便在“stringification”数据之前转换数据。鉴于此,让我们将encode和decode重命名为toJSON和fromJSON。

class User {

  /* ... */

  toJSON(): UserJSON {
    return Object.assign({}, this, {
      created: this.created.toString()
    });
  }

  static fromJSON(json: UserJSON): User {
    let user = Object.create(User.prototype);
    return Object.assign(user, json, {
      created: new Date(json.created)
    });
  }
}


我们不再需要显式地调用user.encode()了

let data = JSON.stringify(new User("Steve", 39));
let user = User.fromJSON(JSON.parse(data));


这很好,但我们可以做得更好。JSON.parse接受第二个名为reviver的参数,该参数是一个函数,在解析对象时,对象中的每个键/值对都会调用该函数。根对象通过一个空字符串作为键传递给reviver。让我们向User类添加一个reviver函数。

class User {

  /* ... */

  static reviver(key: string, value: any): any {
    return key === "" ? User.fromJSON(value) : value;
  }
}


现在我们可以这样写:

let user = JSON.parse(data, User.reviver);


不是太寒酸…

使用这种模式的好处是它的合成非常好。假设User拥有一个包含account实例的account属性

class User {

  private account: Account;

  /* ... */

  static fromJSON(json: UserJSON): User {
    let user = Object.create(User.prototype);
    return Object.assign(user, json, {
      created: new Date(json.created),
      account: Account.fromJSON(json.account)
    });
  }
}


这是完整的注释User类。

class User {

  private created: Date;

  constructor(
    private name: string,
    private age:  string
  ) {
    this.created = new Date();
  }

  getName(): string {
    return this.name;
  }

  // toJSON is automatically used by JSON.stringify
  toJSON(): UserJSON {
    // copy all fields from this to an empty object and return in
    return Object.assign({}, this, {
      // convert fields that need converting
      created: this.created.toString()
    });
  }

  // fromJSON is used to convert an serialized version
  // of the User to an instance of the class
  static fromJSON(json: UserJSON|string): User {
    if (typeof json === 'string') {
      // if it's a string, parse it first
      return JSON.parse(json, User.reviver);
    } else {
      // create an instance of the User class
      let user = Object.create(User.prototype);
      // copy all the fields from the json object
      return Object.assign(user, json, {
        // convert fields that need converting
        created: new Date(json.created),
      });
    }
  }

  // reviver can be passed as the second parameter to JSON.parse
  // to automatically call User.fromJSON on the resulting value.
  static reviver(key: string, value: any): any {
    return key === "" ? User.fromJSON(value) : value;
  }
}

// A representation of User's data that can be converted to
// and from JSON without being altered.
interface UserJSON {
  name:    string;
  age:     number;
  created: string;
}