一起养成写作习惯!这是我参与「掘金日新计划 · 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可以考虑:
- any
- 用 strings,numbers,arrays
- 用已知函数名开头的 strings,numbers,arrays
- 让每个函数能够获得正确的参数个数
- 让每个函数的参数能够获得类型
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 带来了更精准的类型,但是依旧有问题:
- 错误提示不够精准
- 波换了ts的自动补全
- 某些情况会有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' ];
再一次证明:当我们过度的追求类型的精确,却导致了我们的类型的不准确。这些不准确可以被纠正:需要更多更全的测试用例。所以说越复杂的代码,需要越多的测试用例。