[译]<<Effective TypeScript>>技巧35:从api/约定中生成 type ,而不是数据

146 阅读2分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第27天,点击查看活动详情

本文的翻译于<<Effective TypeScript>>, 特别感谢!! ps: 本文会用简洁, 易懂的语言描述原书的所有要点. 如果能看懂这文章,将节省许多阅读时间. 如果看不懂,务必给我留言, 我回去修改.

技巧35:从api/约定中生成 type ,而不是数据

有的时候,不一定要自己写type。我们可以从api/约定中生成 type,也可以从数据中生成type。我们要尽可能的从前者生成,因为前者能让ts保证我们通过所有的测试用例。后者却无法做到。

例如在技巧31中,我们写了一个函数用于计算 GeoJson几何形状的边界框:

function calculateBoundingBox(f: GeoJSONFeature): BoundingBox | null {
  let box: BoundingBox | null = null;

  const helper = (coords: any[]) => {
    // ...
  };

  const {geometry} = f;
  if (geometry) {
    helper(geometry.coordinates);
  }
 return box;
}

技巧31中GeoJSONFeature的type写的不是很完美。其实更好的做法是从正式的GeoJSON规范生成type。幸运的是,有人帮我们生成好了,我们可以这样获取:

$ npm install --save-dev @types/geojson
+ @types/geojson@7946.0.7

当你安装了上面的类型声明,ts立马就能发现代码中的错误:

import {Feature} from 'geojson';

function calculateBoundingBox(f: Feature): BoundingBox | null {
  let box: BoundingBox | null = null;

  const helper = (coords: any[]) => {
    // ...
  };
const {geometry} = f;
  if (geometry) {
    helper(geometry.coordinates);
                 // ~~~~~~~~~~~
                 // Property 'coordinates' does not exist on type 'Geometry'
                 //   Property 'coordinates' does not exist on type
                 //   'GeometryCollection'
  }

  return box;
}

错误的原因:代码默认geometry拥有属性coordinates。但是GeoJSON中有个数据结构GeometryCollection是没有属性coordinates。如果你调用该函数同时将GeometryCollection作为参数传入。将会发生严重的错误。

我们可以这样解决这个报错:

onst {geometry} = f;
if (geometry) {
  if (geometry.type === 'GeometryCollection') {
    throw new Error('GeometryCollections are not supported.');
  }
  helper(geometry.coordinates);  // OK
}

但是还有更好的解决办法,对数据结构GeometryCollection也提供支持:

const geometryHelper = (g: Geometry) => {
  if (geometry.type === 'GeometryCollection') {
    geometry.geometries.forEach(geometryHelper);
  } else {
    helper(geometry.coordinates);  // OK
  }
}

const {geometry} = f;
if (geometry) {
  geometryHelper(geometry);
}

你可能在实际工作中没有碰到数据结构GeometryCollection,使用官方规范生成的type,能让我们的代码更健壮。同时让我们对自己的代码更自信。

还有一个类似的例子关于GraphQL。GraphQL拥有和ts类似的类型系统。例如,在GraphQL中写查询语句请求接口中的特定字段:

query {
  repository(owner: "Microsoft", name: "TypeScript") {
    createdAt
    description
  }
}

查询结果如下:

{
  "data": {
    "repository": {
      "createdAt": "2014-06-17T15:28:39Z",
      "description":
        "TypeScript is a superset of JavaScript that compiles to JavaScript."
    }
  }
}

GraphQL中的类型系统能帮助你清晰的确定:某个变量是不是为空。

以下是获取GitHub存储库开源许可证的查询:

uery getLicense($owner:String!, $name:String!){
  repository(owner:$owner, name:$name) {
    description
    licenseInfo {
      spdxId
      name
    }
  }
}

ownerowner和name是GraphQL变量,它们本身就是类型化的。这其实是和ts很相似。但是要也有不同:这些变量在GraphQL可以为空,在ts中不可以。 有很多工具帮你将GraphQL的查询转成ts类型,例如Apollo:

$ apollo client:codegen \
    --endpoint https://api.github.com/graphql \
    --includes license.graphql \
    --target typescript
Loading Apollo Project
Generating query files with 'typescript' target - wrote 2 files

转化成ts类型后输出如下:

export interface getLicense_repository_licenseInfo {
  __typename: "License";
  /** Short identifier specified by <https://spdx.org/licenses> */
  spdxId: string | null;
  /** The license full name specified by <https://spdx.org/licenses> */
  name: string;
}
export interface getLicense_repository {
  __typename: "Repository";
  /** The description of the repository. */
  description: string | null;
  /** The license associated with the repository */
  licenseInfo: getLicense_repository_licenseInfo | null;
}

export interface getLicense {
  /** Lookup a given repository by the owner and repository name. */
  repository: getLicense_repository | null;
}

export interface getLicenseVariables {
  owner: string;
  name: string;
}

这样的type会随着GraphQL的规范自动更新。保证你的type永远是正确的。当你无法从api/规范中生成type,你也可以用quicktype之类的工具从数据中生成type。

但是从数据中生成type有风险:可能有你没有遇到过的数据!