[译]<<Effective TypeScript>>技巧29:函数类型设计:宽进严出

413 阅读1分钟

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

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

技巧29:函数类型设计:宽进严出

稳健性原则,也称作波斯特尔定律。波斯特尔在TCP的背景中写下这个原则:

TCP实现应该遵循健壮性的一般原则:在你做的事情上要保守,在你接受他人的事情上要自由。

这一原则应用于函数的设计上就是:宽进严出。函数入参类型越广越好,函数输出越具体越好。

这有个例子:例如,3D mapping API可以提供一种方法来定位相机并计算边界框的视口:

declare function setCamera(camera: CameraOptions): void;
declare function viewportForBounds(bounds: LngLatBounds): CameraOptions;

viewportForBounds 的输出可直接传入:setCamera。另外还有其他的一些 types:

interface CameraOptions {
  center?: LngLat;
  zoom?: number;
  bearing?: number;
  pitch?: number;
}

type LngLat =
{ lng: number; lat: number; } |
  { lon: number; lat: number; } |
  [number, number];
  
type LngLatBounds =
  {northeast: LngLat, southwest: LngLat} |
  [LngLat, LngLat] |
  [number, number, number, number];

CameraOptions 的字段都是可选的。LngLatBounds是联合类型。这让viewportForBounds() 和 setCamera() 调用起来非常方便。

现在,让我们编写一个函数,调整viewport以适应GeoJSON功能,并将新viewport存储在URL中(有关CalculateBondingBox的定义,请参见技巧 31 ):

function focusOnFeature(f: Feature) {
  const bounds = calculateBoundingBox(f);
  const camera = viewportForBounds(bounds);
  setCamera(camera);
  const {center: {lat, lng}, zoom} = camera;
               // ~~~      Property 'lat' does not exist on type ...
               //      ~~~ Property 'lng' does not exist on type ...
  zoom;  // Type is number | undefined
  window.location.search = `?v=@${lat},${lng}z${zoom}`;
}

这报错的原因:viewportForBounds() 函数输出的变量类型CameraOptions都是可选字段, 所以center,zoom都有可能为undefine。简而言之:viewportForBounds()参数设计上宽入宽出,不符合宽入严出的原则!

我们的解决办法:定义非可选类型 Camera,同时对应生成可选类型:CameraLike。

interface LngLat { lng: number; lat: number; };
type LngLatLike = LngLat | { lon: number; lat: number; } | [number, number];

interface Camera {
  center: LngLat;
  zoom: number;
  bearing: number;
  pitch: number;
}
interface CameraOptions extends Omit<Partial<Camera>, 'center'> {
  center?: LngLatLike;
}
type LngLatBounds =
  {northeast: LngLatLike, southwest: LngLatLike} |
  [LngLatLike, LngLatLike] |
  [number, number, number, number];

declare function setCamera(camera: CameraOptions): void;
declare function viewportForBounds(bounds: LngLatBounds): Camera;

上面的 CameraOptions 等价于:

interface CameraOptions {
  center?: LngLatLike;
  zoom?: number;
  bearing?: number;
  pitch?: number;
}

重要的改变在于:

  1. 将宽松的类型 CameraLike 用于函数 viewportForBounds 的入参
  2. 将严格的类型 Camera 用于函数 viewportForBounds 出参

然后就发现能解决报错的问题:

function focusOnFeature(f: Feature) {
  const bounds = calculateBoundingBox(f);
  const camera = viewportForBounds(bounds);
  setCamera(camera);
  const {center: {lat, lng}, zoom} = camera;  // OK
  zoom;  // Type is number
  window.location.search = `?v=@${lat},${lng}z${zoom}`;
}