一些让程序保持可扩展的 TypeScript 技巧
我们使用 TypeScript 的理由是,它有助于开发更快更安全的 app。
TypeScript 默认会做很多简化,这有助于开发者更容易的上手,但从长远来看,这些简化也会浪费不少的开发时间。
我们收集了一系列更为严格的 TypeScript 编码规则。只要你习惯一次,将来就会为你省下大量的编码时间。
any
这是一条很简单的规则,但长远看我们可以从中得到大大的好处。
永远都不要使用 “any”,永远!!!
原因很简单,因为不会有任何场景需要使用 “any” 来描述一个类型。如果你遇到了使用 any 的情况,可能是架构、代码遗留或者其他特殊问题。
使用 泛型,未知类型 或者 重载 然后就不用担心数据结构上会出现意料之外的问题了。而这类问题的调试成本通常都很高。
严格模式
TypeScript 有 “严格” 模式,不过遗憾的这个模式默认处于关闭状态。此模式下有一系列的规则可以让 TypeScript 用起来更安全舒适。如果不了解此模式,参考 这篇文章。
在 “严格” 模式下,你可以完全不用担心诸如 undefined is not a function
、cannot read property X of null
等错误。你的类型定义将会无比的准确。
我得怎样做?
如果你开了一个新项目,享受 “严格模式” 的乐趣吧。
如果你有一个非 “严格模式” 的项目,然后你还想为此项目开启严格模式,那么你首先看到的就是一堆编译问题。如果没有编辑器的警告的话,编写满足严格模式的代码是一件非常困难的事情,所以你在打开严格模式后很可能会碰到很多有问题的地方。因此迁移整个项目到 “严格模式” 很快就能让人感到烦躁。
对此的建议是将这个大任务切割为小块儿。“严格” 模式由 6 条规则组成。可以启用其中的一条规则并且修复所有的错误。下一次启用第二条规则,修复错误并且循环往复。终有一天会完全迁移到 “严格” 模式的。
tsconfig.json 文件
{
// ...,
"compilerOptions": {
// 一组很溜的规则配置
"noImplicitAny": true,
"noImplicitThis": true,
"strictNullChecks": true,
"strictPropertyInitialization": true,
"strictBindCallApply": true,
"strictFunctionTypes": true,
// 支持上述六个规则的快捷配置
"strict": true,
// ...
}
}
只读
对我们 TypeScript 开发者来说,下一个重要的规则是无时无刻使用只读。
在处理的过程中改变数据结构是一种不好的实践。举个栗子,Angular 就不喜欢这种处理方式:当转换数据时,视图的变更检测和视图更新会出现一些问题。
但你完全可以轻松的阻止所有的数据更改,只要养成写 readonly 关键字的习惯。
我们应该怎么做呢?
应用中有很多地方可以将不安全的类型替换为只读类型。
在 interface 中使用 “readonly” 属性
// 之前
export interface Thing {
data: string;
}
// 之后
export interface Thing {
readonly data: string;
}
首选 “readonly” 类型
// 之前
export type UnsafeType = { prop: number };
// 之后
export type SafeType = Readonly<{ prop: number }>; // Prefer “readonly” types
在类中尽可能的使用 “readonly” 字段
// 之前
class UnsafeComponent {
loaderShow$ = new BehaviorSubject<boolean>(true);
}
// 之后
class SafeComponent {
readonly loaderShow$ = new BehaviorSubject<boolean>(true);
}
使用 “readonly” 结构
// 之前
const unsafeArray: Array<number> = [1, 2, 3];
const unsafeArrayOtherWay: number[] = [1, 2, 3];
// 之后
const safeArray: ReadonlyArray<number> = [1, 2, 3];
const safeArrayOtherWay: readonly number[] = [1, 2, 3];
// 三种写法,三种安全等级
const unsafeArray: number[] = [1, 2, 3]; // 糟糕的
const safeArray: readonly number[] = [1, 2, 3]; // 还不错
const verySafeTuple: readonly [number, number, number] = [1, 2, 3]; // 最棒的
// 映射:
// 之前
const unsafeMap: Map<string, number> = new Map<string, number>();
// 之后
const safeMap: ReadonlyMap<string, number> = new Map<string, number>();
// 集合:
// 之前
const unsafeSet: Set<number> = new Set<number>();
// 之后
const safeSet: ReadonlySet<number> = new Set<number>();
设为常量
在 TypeScript v.3.4 中我们可以使用 常量断言 。
这是一个相比 “readonly” 更加严格的限制手段,因为它将实例封装为最准确的类型,你可以理解为没有任何情况可以改变它的值。
设为常量
后还可以在 IDE 中获取到准确的类型提示。
实用类型
TypeScript 有一组特殊的类型,这些类型是其他类型的快捷转换形式。
请查阅 实用类型的官方文档 并且在 app 中使用他们。这将会为你节约很多时间。
缩小你的类型范围
TypeScript 有很多工具可以缩小类型范围。这个特性很有用,因为我们可以在项目中支持各种各样场景下的严格限制的输入。
看下如下样例。这是一个很简单的例子,不过它可以帮助我们理解类型检查和类型范围缩小的区别。
import {Subject} from 'rxjs';
import {filter} from 'rxjs/operators';
interface Data {
readonly greeting: string;
}
const data$$ = new Subject<Data | null>();
/**
* source$ 指定了类型 "Observable<Data | null>"
* 然而 "null" 并不能通过此函数过滤器
*
* 这是因为 typescript 不能确定类型是否产生了变化。
* 函数 “value => !!value” 返回了布尔值但是未明确声明类型
*/
const source$ = data$$.pipe(
filter(value => !!value)
)
/**
* 如下是一些比较好的例子
*
* 箭头函数返回 “是否为 Data 类型的数据” 的结果
* 它缩小了类型范围,并使 "wellTypedSource$" 拥有了正确的类型
*/
const wellTypedSource$ = data$$.pipe(
filter((value): value is Data => !!value)
)
// 如下代码不能被编译,你可以试下
// source$.subscribe(x => console.log(x.greeting));
wellTypedSource$.subscribe(x => console.log(x.greeting));
data$$.next({ greeting: 'Hi!' });
你可以通过如下方法缩小类型范围:
**typeof**
来自 Javascript 的操作符,用于检查原始类型**instanceof**
来自 Javascript 的操作符,用于检查被继承的实例**is T**
TypeScript 的声明,允许检查复杂类型以及接口。不过你得小心的使用这个功能,因为确保类型正确是你的责任,而不是 TypeScript 的。
一些使用样例:
// typeof 限制
function getCheckboxState(value: boolean | null): string {
if (typeof value === 'boolean') {
// value 只能是 "布尔" 类型
return value ? 'checked' : 'not checked';
}
/**
* 在此范围内,值可能为 “null” 类型
*/
return 'indeterminate';
}
// instanceof 限制
abstract class AbstractButton {
click(): void { }
}
class Button extends AbstractButton {
click(): void { }
}
class IconButton extends AbstractButton {
icon = 'src/icon';
click(): void { }
}
function getButtonIcon(button: AbstractButton): string | null {
/**
* 在 "instanceof" 后 TS 就能知道数据类型是 "icon"
* field
*/
return button instanceof IconButton ? button.icon : null;
}
// is T 限制
interface User {
readonly id: string;
readonly name: string;
}
function isUser(candidate: unknown): candidate is User {
return (
typeof candidate === "object" &&
candidate !== null &&
"id" in candidate &&
"name" in candidate
);
}
const someData = { id: '42', name: 'John' };
if (isUser(someData)) {
/**
* TS 现在知道了 someData 实现了 User 接口
*/
console.log(someData.id, someData.name)
}
总结
特别是在安全且严格的 TS 开发过程中,这些技术在团队中起着很重要的作用。当然,它们也不能解决所有的问题,不过如果你一旦开始使用,这将使你能够在大型项目中避开一些非预期的棘手的错误。
这篇文章是进阶 Angular - 大型 Angular 交互手册的第二章。你可继续浏览 Angular 相关的内容:Angular 研究会
本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。