本文的翻译于<<Effective TypeScript>>, 特别感谢!! ps: 本文会用简洁, 易懂的语言描述原书的所有要点. 如果能看懂这文章,将节省许多阅读时间. 如果看不懂,务必给我留言, 我回去修改.
技巧37:给名词类type添加「标识」
在技巧4中曾经讨论过的类型:
interface Vector2D {
x: number;
y: number;
}
function calculateNorm(p: Vector2D) {
return Math.sqrt(p.x * p.x + p.y * p.y);
}
calculateNorm({x: 3, y: 4}); // OK, result is 5
const vec3D = {x: 3, y: 4, z: 1};
calculateNorm(vec3D); // OK! result is also 5
如何让calculateNorm拒绝3D向量?
其中的一个方法用命名类型,这里引入一个字段「brand」:
nterface Vector2D {
_brand: '2d';
x: number;
y: number;
}
function vec2D(x: number, y: number): Vector2D {
return {x, y, _brand: '2d'};
}
function calculateNorm(p: Vector2D) {
return Math.sqrt(p.x * p.x + p.y * p.y); // Same as before
}
calculateNorm(vec2D(3, 4)); // OK, returns 5
const vec3D = {x: 3, y: 4, z: 1};
calculateNorm(vec3D);
// ~~~~~ Property '_brand' is missing in type...
brand保证了向量来自正确的地方。但是无法阻止你将 _brand: '2d';添加到3D向量中。所以说brand只能预防无意导致的错误,无法预防恶意导致的错误。
我们再来看一个例子:如果你写一个函数,输入的参数是保证为绝对路径。这在js运行时非常好解决。但是怎么在ts类型系统中判断绝对路径呢?
type AbsolutePath = string & {_brand: 'abs'};
function listAbsolutePath(path: AbsolutePath) {
// ...
}
function isAbsolutePath(path: string): path is AbsolutePath {
return path.startsWith('/');
}
这样ts就能检查string是不是绝对路径:
function f(path: string) {
if (isAbsolutePath(path)) {
listAbsolutePath(path);
}
listAbsolutePath(path);
// ~~~~ Argument of type 'string' is not assignable
// to parameter of type 'AbsolutePath'
}
这种方法能主动引导函数调用者,去检查自己的变量是不是绝对路径!
这样的方法也能对很多特殊类型进行建模。这些特殊类型很可能是ts无法表达的。比如:一个有序的数组。
这有个例子:用二分法在有序数组中查找元素:
function binarySearch<T>(xs: T[], x: T): boolean {
let low = 0, high = xs.length - 1;
while (high >= low) {
const mid = low + Math.floor((high - low) / 2);
const v = xs[mid];
if (v === x) return true;
[low, high] = x > v ? [mid + 1, high] : [low, mid - 1];
}
return false;
}
如果用该函数对有序数组排序能得到正确结果,但是如果是无序数组就不行。但是你无法用ts表达有序数组类型。但是你可以通过brand来做:
type SortedList<T> = T[] & {_brand: 'sorted'};
function isSorted<T>(xs: T[]): xs is SortedList<T> {
for (let i = 1; i < xs.length; i++) {
if (xs[i] > xs[i - 1]) {
return false;
}
}
return true;
}
function binarySearch<T>(xs: SortedList<T>, x: T): boolean {
// ...
}
这样也能强制让用户对自己的数组进行检查。
我们不仅可以对object添加标签,也可以对number之类的简单类型添加标签:
type Meters = number & {_brand: 'meters'};
type Seconds = number & {_brand: 'seconds'};
const meters = (m: number) => m as Meters;
const seconds = (s: number) => s as Seconds;
const oneKm = meters(1000); // Type is Meters
const oneMin = seconds(60); // Type is Seconds
虽然这种方法很尴尬,进行一次计算之后,会将brand消除:
const tenKm = oneKm * 10; // Type is number
const v = oneKm / oneMin; // Type is number
但是如果你的代码设计很多单位计算,这种方法依旧有优势!