一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第1天,点击查看活动详情。
本文的翻译于<<Effective TypeScript>>, 特别感谢!! ps: 本文会用简洁, 易懂的语言描述原书的所有要点. 如果能看懂这文章,将节省许多阅读时间. 如果看不懂,务必给我留言, 我回去修改.
了解 TypeScript
技巧1: TypeScript 和 JavaScript 的关系
ts 是 js 的超集, 这个超集包括几个特点:
-
js 代码天然就是ts代码, 下面用 ts-node-dev 运行, 能顺利运行
function greet(who) { console.log('Hello', who); } greet('lala') -
ts代码却不一定是合法的 js代码, ts有自己独特的语法
ts 增加了类型系统 , 用来在js运行前发现错误:
let city = 'new york city';
console.log(city.toUppercase());
// ~~~~~~~~~~~ Property 'toUppercase' does not exist on type
// 'string'. Did you mean 'toUpperCase'?
但是ts不能完全发现错误, 例如ts无法发现数组越界的错误
const names = ['Alice', 'Bob'];
console.log(names[2].toUpperCase());
部分js语法在ts中会被禁止使用:
const a = null + 7; // Evaluates to 7 in JS
// ~~~~ Operator '+' cannot be applied to types ...
const b = [] + 12; // Evaluates to '12' in JS
// ~~~~~~~ Operator '+' cannot be applied to types ...
alert('Hello', 'TypeScript'); // alerts "Hello"
// ~~~~~~~~~~~~ Expected 0-1 arguments, but got 2
技巧2: 了解 ts 的配置
一个项目的ts配置,可以在项目根目录下的tsconfig.json文件中查看. 其中两个重要的配置需要了解: noImplicitAny 和strictNullChecks
-
noImplicitAny
当编辑tsconfig.json文件:
{ "compilerOptions": { "noImplicitAny": false } }下面代码是可以通过ts的类型检查的:
function add(a, b) { return a + b; } add(10, null);当编辑tsconfig.json文件:
{ "compilerOptions": { "noImplicitAny": true } }会出现报错:
function add(a, b) { // ~ Parameter 'a' implicitly has an 'any' type // ~ Parameter 'b' implicitly has an 'any' type return a + b; }这有利于我们更好的捕捉错误.所以我们应该尽量将noImplicitAny设置为true
-
strictNullChecks
当编辑tsconfig.json文件:{ "compilerOptions": { "strictNullChecks": false } }下面代码是可以通过ts的类型检查的:
const x: number = null;当编辑tsconfig.json文件:
{ "compilerOptions": { "strictNullChecks": true } }会出现报错:
const x: number = null; // ~ Type 'null' is not assignable to type 'number'这有利于我们更好的避免null和undefine引起的错误.所以我们应该尽量将strictNullChecks设置为true
技巧3: 理解ts的代码生成与类型检查之间的相互独立
tsc(ts的编译器)只做两件事:
- 将ts代码转成低版本的js代码,以便运行在浏览器上
- 检查你代码中的类型错误 记得这一点: 上述两件事是完全独立的!
独立会造成什么影响?
-
影响一: 当你的代码类型错误, 依旧会生成js文件
$ cat test.ts let x = 'hello'; x = 1234; $ tsc test.ts test.ts:2:1 - error TS2322: Type '1234' is not assignable to type 'string' 2 x = 1234; ~ $ cat test.js var x = 'hello'; x = 1234;这样有利有弊
好处在于 当我们项目急着上线, 那我们可以先生成可运行的js代码, 之后有时间再来修改类型错误
不好在于 你不知道哪天这个会变成严重的错误
如果我们目标定为0错误才生成js代码.那么我们可以在tsconfig.json文件中设置noEmitOnError为 true
-
影响二: 我们不能在代码运行检查ts的类型
例如下面代码:
interface Square { width: number; } interface Rectangle extends Square { height: number; } type Shape = Square | Rectangle; function calculateArea(shape: Shape) { if (shape instanceof Rectangle) { // ~~~~~~~~~ 'Rectangle' only refers to a type, // but is being used as a value here return shape.width * shape.height; // ~~~~~~ Property 'height' does not exist // on type 'Shape' } else { return shape.width * shape.width; } }instanceof 用于检查变量类型, 但是它只会在代码运行时候检查. 但是在代码运行前, tsc在生成js文件的时候, 会将所有ts 的types, interfaces 擦除.
对于这类问题可以这么解决:
第一种方法:
function calculateArea(shape: Shape) { if ('height' in shape) { shape; // Type is Rectangle return shape.width * shape.height; } else { shape; // Type is Square return shape.width * shape.width; } }第二种方法:
interface Square { kind: 'square'; width: number; } interface Rectangle { kind: 'rectangle'; height: number; width: number; } type Shape = Square | Rectangle; function calculateArea(shape: Shape) { if (shape.kind === 'rectangle') { shape; // Type is Rectangle return shape.width * shape.height; } else { shape; // Type is Square return shape.width * shape.width; } }第三种方法:
class Square { constructor(public width: number) {} } class Rectangle extends Square { constructor(public width: number, public height: number) { super(width); } } type Shape = Square | Rectangle; function calculateArea(shape: Shape) { if (shape instanceof Rectangle) { shape; // Type is Rectangle return shape.width * shape.height; } else { shape; // Type is Square return shape.width * shape.width; // OK } } -
影响三:类型操作无法影响运行时变量的值
例如下面代码:
function asNumber(val: number | string): number { return val as number; }tsc会生成如下代码:
function asNumber(val) { return val; }我们可以这样解决:
function asNumber(val: number | string): number { return typeof(val) === 'string' ? Number(val) : val; } -
影响四:代码运行时变量的类型, 和我们预先声明的类型,可能不一样 这类问题常见与从接口中获取数据:
interface LightApiResponse { lightSwitchValue: boolean; } async function setLight() { const response = await fetch('/light'); const result: LightApiResponse = await response.json(); setLightSwitch(result.lightSwitchValue); }我们从接口中获取的response不一定是我们预先定义的LightApiResponse类型
-
影响五: 我们不能通过ts的类型来申明函数重载
下列代码会报错:
function add(a: number, b: number) { return a + b; } // ~~~ Duplicate function implementation function add(a: string, b: string) { return a + b; } // ~~~ Duplicate function implementation我们可以通过下面的方法申明函数重载:
function add(a: number, b: number): number; function add(a: string, b: string): string;
function add(a, b) {
return a + b;
}
const three = add(1, 2); // Type is number
const twelve = add('1', '2'); // Type is string
```
6. 影响六: ts 的类型系统不会代码影响代码运行性能 所有ts的类型定义, 操作都会在运行前被擦除, 所以ts的类型系统不会对代码运行带来任何负担
技巧四: 熟悉ts的类型兼容
掌握ts的类型兼容就能写出更健壮的代码.
假设你有一个二维的type:
interface Vector2D {
x: number;
y: number;
}
你写了一个函数计算该向量到原点的距离:
function calculateLength(v: Vector2D) {
return Math.sqrt(v.x * v.x + v.y * v.y);
}
同时你还有一个可以命名的二维的type
interface NamedVector {
name: string;
x: number;
y: number;
}
尝试运行一下, 下列代码, 发现能正常运行.
const v: NamedVector = { x: 3, y: 4, name: 'Zee' };
calculateLength(v); // OK, result is 5
这里可以得出结论:
NamedVector有Vector2D所有的属性, 那么函数calculateLength如果能够接受Vector2D类型的变量, 那也一定能接受NamedVector类型的变量.
但是这个类型兼容会导致一个问题:
例如当你有一个三维的type:
interface Vector3D {
x: number;
y: number;
z: number;
}
如果你想计算三维空间的点到原点的距离,正确的计算公式应该是:
Math.sqrt(v.x * v.x + v.y * v.y + v.z*v.z)
直接调用上面的calculateLength函数显然是错误的, 但是ts并不会报错!, 因为其满足ts类型兼容型原则
所以请记住一个原则: 尽量不要对遍历一个对象
请看如下的代码
function calculateLengthL1(v: Vector3D) {
let length = 0;
for (const axis of Object.keys(v)) {
const coord = v[axis];
// ~~~~~~~ Element implicitly has an 'any' type because ...
// 'string' can't be used to index type 'Vector3D'
length += Math.abs(coord);
}
return length;
}
为什么会报错? 因为你虽然定义了变量v是Vector3D类型, 但是由于类型兼容原则. 你无法保证, 用户会传什么类型的数据.
例如:下面这个类型也能传入函数calculateLengthL1
interface VectorND {
x: number;
y: number;
z: number;
p: number;
s: string;
}
那我们改如何解决这个问题, 不用循环来遍历对象:
function calculateLengthL1(v: Vector3D) {
return Math.abs(v.x) + Math.abs(v.y) + Math.abs(v.z);
}
类型兼容当遇到class也有可能出问题:
class C {
foo: string;
constructor(foo: string) {
this.foo = foo;
}
}
const c = new C('instance of C');
const d: C = { foo: 'object literal' }; // OK!
由于类型兼容, 导致d也能分配给C
但是类型兼容对写测试用例其实是有好处的! 例如你写了一个函数查询数据集,并且处理结果:
interface Author {
first: string;
last: string;
}
function getAuthors(database: PostgresDB): Author[] {
const authorRows = database.runQuery(`SELECT FIRST, LAST FROM AUTHORS`);
return authorRows.map(row => ({first: row[0], last: row[1]}));
}
想要测试这个函数, 你当然可以写PostgresDB类型数据的mock, 但是有一个更好的办法:
interface DB {
runQuery: (sql: string) => any[];
}
function getAuthors(database: DB): Author[] {
const authorRows = database.runQuery(`SELECT FIRST, LAST FROM AUTHORS`);
return authorRows.map(row => ({first: row[0], last: row[1]}));
}
在使用这个函数的时候, 可以传入PostgresDB类型的数据. 但是在写测试用例的时候将变得更容易:
test('getAuthors', () => {
const authors = getAuthors({
runQuery(sql: string) {
return [['Toni', 'Morrison'], ['Maya', 'Angelou']];
}
});
expect(authors).toEqual([
{first: 'Toni', last: 'Morrison'},
{first: 'Maya', last: 'Angelou'}
]);
});
技巧五: 避免使用any
使用any能够让你逃过类型检查, 例如:
let age: number;
age = '12';
// ~~~ Type '"12"' is not assignable to type 'number'
age = '12' as any; // OK
但是请你务必要了解使用any带来的危害:
-
使用any将失去ts的类型安全检查.
age += 1; // OK; at runtime, age is now "121" -
使用any,可能会导致js偷偷转换你数据的类型
function calculateAge(birthDate: Date): number { // ... } let birthDate: any = '1990-01-19'; calculateAge(birthDate); // OK你传入birthDate是一个字符串, 但是有可能在函数中被转换为number
-
将失去ts带来的一些便利
使用ts能帮助代码编辑器, 实现代码自动补全功能, 例如:
-
any 影藏了你的类型设计 设计一个好的类型, 对于一个干净, 健壮, 简洁的代码非常重要, 用了any, 你的同事或者合作伙伴看不到你的类型设计