一些让程序保持可扩展的 TypeScript 技巧

Vue.js

我们使用 TypeScript 的理由是,它有助于开发更快更安全的 app。

TypeScript 默认会做很多简化,这有助于开发者更容易的上手,但从长远来看,这些简化也会浪费不少的开发时间。

我们收集了一系列更为严格的 TypeScript 编码规则。只要你习惯一次,将来就会为你省下大量的编码时间。

any

这是一条很简单的规则,但长远看我们可以从中得到大大的好处。

永远都不要使用 “any”,永远!!!

原因很简单,因为不会有任何场景需要使用 “any” 来描述一个类型。如果你遇到了使用 any 的情况,可能是架构、代码遗留或者其他特殊问题。

使用 泛型未知类型 或者 重载 然后就不用担心数据结构上会出现意料之外的问题了。而这类问题的调试成本通常都很高。

严格模式

TypeScript 有 “严格” 模式,不过遗憾的这个模式默认处于关闭状态。此模式下有一系列的规则可以让 TypeScript 用起来更安全舒适。如果不了解此模式,参考 这篇文章

在 “严格” 模式下,你可以完全不用担心诸如 undefined is not a functioncannot 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 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://medium.com/its-tinkoff/typescrip...

译文地址:https://learnku.com/vuejs/t/52188

本文为协同翻译文章,如您发现瑕疵请点击「改进」按钮提交优化建议
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!