[译]<<Effective TypeScript>> 技巧34 使用函数构造和库来帮助类型流动

113 阅读3分钟

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

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

技巧34 宁愿类型不完整,也不要类型不正确

当我们对一个变量声明type的时候,我们会有很多选择,我们可以声明精确的type,也可以声明不那么精确的type。有时候精确的类型能更好的帮我们捕捉错误,也能让我使用 ts提供的服务。但是type越精确也容易出错,我们宁愿选择不那么精确的type,也不要选择错误的type。

假定你在为 GeoJSON 写 type ,其中有三种形状:点,线,面:

interface Point {
  type: 'Point';
  coordinates: number[];
}
interface LineString {
  type: 'LineString';
  coordinates: number[][];
}
interface Polygon {
  type: 'Polygon';
  coordinates: number[][][];
}
type Geometry = Point | LineString | Polygon;  // Also several others

这样声明是有有效的,但是 number[] 定义过于宽泛了。不够精准。前面的 number[] 其实是经纬度。所以可以这样定义:

type GeoPosition = [number, number];
interface Point {
  type: 'Point';
  coordinates: GeoPosition;
}
// Etc.

当你以为定义这样精准的type能获得认可。不幸的是:坐标包含不止经纬度,还包括海拔等其他参数。这样的 type 就是错误的!

另外,GeoJSON还包含类似Lisp的语言:

12
"red"
["+", 1, 2]  // 3
["/", 20, 2]  // 10
["case", [">", 20, 10], "red", "blue"]  // "red"
["rgb", 255, 0, 127]  // "#FF007F"

当你想为上面的数据定义 type ,有几种不同精度的type可以考虑:

  1. any
  2. 用 strings,numbers,arrays
  3. 用已知函数名开头的 strings,numbers,arrays
  4. 让每个函数能够获得正确的参数个数
  5. 让每个函数的参数能够获得类型

1,2精度的type非常直接:

type Expression1 = any;
type Expression2 = number | string | any[];

当然方法1,2,不够精准。很多错误的测试用例无法通过:

const tests: Expression2[] = [
  10,
  "red",
  true,
// ~~~ Type 'true' is not assignable to type 'Expression2'
  ["+", 10, 5],
  ["case", [">", 20, 10], "red", "blue", "green"],  // Too many values
  ["**", 2, 31],  // Should be an error: no "**" function
  ["rgb", 255, 128, 64],
  ["rgb", 255, 0, 127, 0]  // Too many values
];

为了进一步精确 type ,数组第一个元素可以定义为字符串联合类型:

type FnName = '+' | '-' | '*' | '/' | '>' | '<' | 'case' | 'rgb';
type CallExpression = [FnName, ...any[]];
type Expression3 = number | string | CallExpression;

const tests: Expression3[] = [
  10,
  "red",
  true,
// ~~~ Type 'true' is not assignable to type 'Expression3'
  ["+", 10, 5],
  ["case", [">", 20, 10], "red", "blue", "green"],
  ["**", 2, 31],
// ~~~~~~~~~~~ Type '"**"' is not assignable to type 'FnName'
  ["rgb", 255, 128, 64]
];

这次type精度提升很多:但是如何保证函数能获得正确的参数个数? 这里有个技巧:需要用到函数的递归:

type Expression4 = number | string | CallExpression;

type CallExpression = MathCall | CaseCall | RGBCall;

interface MathCall {
  0: '+' | '-' | '/' | '*' | '>' | '<';
  1: Expression4;
  2: Expression4;
  length: 3;
}

interface CaseCall {
  0: 'case';
  1: Expression4;
  2: Expression4;
  3: Expression4;
  length: 4 | 6 | 8 | 10 | 12 | 14 | 16 // etc.
}

interface RGBCall {
  0: 'rgb';
  1: Expression4;
  2: Expression4;
  3: Expression4;
  length: 4;
}

const tests: Expression4[] = [
  10,
  "red",
  true,
// ~~~ Type 'true' is not assignable to type 'Expression4'
  ["+", 10, 5],
  ["case", [">", 20, 10], "red", "blue", "green"],
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
//  Type '["case", [">", ...], ...]' is not assignable to type 'string'
  ["**", 2, 31],
// ~~~~~~~~~~~~ Type '["**", number, number]' is not assignable to type 'string
  ["rgb", 255, 128, 64],
  ["rgb", 255, 128, 64, 73]
// ~~~~~~~~~~~~~~~~~~~~~~~~ Type '["rgb", number, number, number, number]'
//                          is not assignable to type 'string'
];

那么现在,错误的测试用例,ts都能检测出来。有意思的一点:你可以用 interface 表示「固定长度的array

Expressions4 带来了更精准的类型,但是依旧有问题:

  1. 错误提示不够精准
  2. 波换了ts的自动补全
  3. 某些情况会有bug,因为这里只允许函数只能接受两个参数,实际则不然:
    const okExpressions: Expression4[] = [
       ['-', 12],
    // ~~~~~~~~~ Type '["-", number]' is not assignable to type 'string'
       ['+', 1, 2, 3],
    // ~~~~~~~~~~~~~~ Type '["+", number, ...]' is not assignable to type 'string'
       ['*', 2, 3, 4],
    // ~~~~~~~~~~~~~~ Type '["*", number, ...]' is not assignable to type 'string'
     ];
    

再一次证明:当我们过度的追求类型的精确,却导致了我们的类型的不准确。这些不准确可以被纠正:需要更多更全的测试用例。所以说越复杂的代码,需要越多的测试用例。