阅读 8729

TypeScript 实践

作者简介:aoto 蚂蚁金服·数据体验技术团队

摘要:之前写过一篇《TypeScript 体系调研报告》,经过半年多的蚂蚁金服数据平台大规模 JS 项目实战,沉淀了一些编程实战经验和感悟。

前言

TypeScript 是有类型定义的 JS 的超集,包括 ES5、ES5+ 和其他一些诸如泛型、类型定义、命名空间等特征的集合,为了大规模 JS 应用而生。对于 TypeScript 本身,更多信息请参考《TypeScript 体系调研报告》。本文只记录 TypeScript 在我们实际项目中产生的一些实际有用的价值。我们的项目基于 React 体系,因此本文着重关注 React 体系和 TypeScript 结合使用的经验。

React in TypeScript

在 TypeScript 开发环境下写 React 组件,与 ES6 的区别主要就是 Props 和 State 的定义。如果是 ES6,大概是这样:

import PropTypes from 'prop-types';
class App extends React.PureComponent {
  state = {
    aState: '',
    bState: '',
  };
}

App.propTypes = {
  aProps: PropTypes.string.isRequired,
  bProps: PropTypes.string.isRequired,
};
复制代码

如果用 TypeScript 来写,大概是这样(本文的 interface 定义默认以 I 开头):

interface IProps {
  aProps: string;
  bProps: string;
}
interface IState {
  aState: string;
  bState: string;
}

class App extends React.PureComponent<IProps, IState> {
  state = {
    aState: '',
    bState: '',
  };
}
复制代码

TypeScript 自带 JSX 解析器,因此为了充分利用它本身的是静态检查功能,所以用泛型来定义 Props 和 State 的类型。如上定义后,在成员方法中通过this.props.this.state.使用 Props 和 State 时可以智能提示,而且会做类型检查。看起来写没有变的简洁多少,但 TypeScript 来开发 React 应用有一个很大的优势,TS 环境中的 React 组件属性是静态的,也就是说可以做静态检查,并且在支持 TS 的 IDE 下写的过程中可以自动自动提示(是否存在以及类型是否正确)/补全,**这对于保障大型 React 应用的代码质量和运行时质量很有帮助。**而 ES6 环境中的 prop-types 属性是动态的,也就是运行期做检查,也不能做自动提示/补全。由此看来除了 JS 代码本身, TypeScript 对 React 组件开发也是很有好处的。

Redux 是最流行的状态管理库,一般和 React 结合使用。在 TypeScript 和 ES6 环境下,Redux 代码写法区别不大,有个问题就是,React 和 Redux 的绑定函数 connect 需用函数写法,而不建议用装饰器写法。因为 TS 对 类装饰器的静态解析还不支持,用了装饰器写 connect 就不能利用 TS 的静态解析的好处了。

const mapStateToProps = (state: {}, ownProps: IProps) => {};
const mapDispatchToProps = {};

export default connect(mapStateToProps, mapDispatchToProps)(App);
复制代码

Antd 是一个流行的 React 组件库,提供了 TS 类型声明,在写组件的时候可以进行属性的自动提示和检查,不确定的可以进入类型声明文件查看,减少了查阅在线 API 文档的次数和属性拼写错误的可能性。

React + Redux + TypeScript 的编程体验:

react+typescript.gif | center | 780x624

面向对象设计

复杂软件需要用复杂的设计,面向对象就是很好的一种设计方式,使用 TS 的一大好处就是 TS 提供了业界认可的类( ES5+ 也支持)、泛型、封装、接口面向对象设计能力,以提升 JS 的面向对象设计能力。

泛型

泛型,简单说就是泛化的类型。我们项目中的泛型实践主要在 React 组件中,这也是借鉴了 React 的 Component 和 PureComponent 在 TS 中的定义。代码如下:

class Component<P, S> {}
class PureComponent<P = {}, S = {}> extends Component<P, S> {}
复制代码

P 和 S 就是泛型,这里的 P 是 Props,S 是 State。从这里可以看到如果我们要基于 React 组件进行基于继承的设计,那泛型就可以发挥作用了。这里举一个例子,假如我们要设计一个复杂 UI 模块,有两层继承,BaseComponent 继承 React.PureComponent,XComponent 继承 BaseComponent,效果就是 XComponent extends BaseComponent extends React.PureComponent,示例代码如下:

export interface IBaseProps {}

export interface IXProps {}

export class BaseComponent<
  IProps extends IBaseProps,
  IState = {}
> extends React.PureComponent<IProps, IState> {}

export class XComponent<
  IProps extends IXProps,
  IState = {}
> extends BaseComponent<IProps, IState> {}
复制代码

BaseComponent 的属性(props)定义是 IProps,继承自 IBaseProps,这样 BaseComponent 组件中就可以使用 IBaseProps 中定义的属性了。

XComponent 的属性定义是 IProps,继承自 IXProps,同时 XComponent 又继承自 BaseComponent,因为我们在 BaseComponent 类的属性泛型定义中 IProps 是继承 IBaseProps,这样两个条件共同作用于 XComponent,就有了 IProps extends IXProps extends IBaseProps 的效果了,就可以在 XComponent 中同时使用 IBaseProps 和 IXProps 的属性了。

封装

我们都知道,封装在对于面向对象软件设计非常有用,而封装各个模块的实现细节,就可以在有效管理软件的复杂度。 TS 对于代码封装性的帮助主要体现在它提供了类似于 Java 的访问控制符。有了 private/protected/public,我们可以自主的控制类需要对外暴露的接口。访问权限需尽可能严,也就是说无需对外暴露的用 private 或者 protected ,如无需被子类使用的就用 private,只有明确需要对外暴露的接口采用 public 来描述。在一些非 React 组件的公共类,封装特性尤为必要。

接口

接口在面向对象设计里面也是很重要的,接口也就是对外提供服务的端口。我们举一个例子,一个公共模块类 A ,提供了几个接口findByIdupdateDatadestroy。那么我们要定义一个接口的定义,方便别的模块使用(这和传统编程语言的接口有点不同,TS 的接口并不会自己运行):

export interface IClassA {
  findById(id: string): IModel;
  updateData(data: IModel): void;
  destroy(): void;
}
复制代码

调用方:

const a = new IClassA();
a.findById('1');
a.updateData({ id: '1', name: 'a' });
a.destroy();
复制代码

有了接口声明,我们在使用该模块的时候可以清晰的看到它到底有哪些接口,方法的入参是什么,返回值是什么。同时也有代码的自动提示,提升开发效率,减少拼写错误导致的低级 Bug。

代码质量

静态检查

现在流行的库如 React 、Redux 、 Lodash 、 Antd 等等都有 TS 类型声明,加上我们自己业务代码完善的类型定义,整个代码库的健康度可以较好的保持住。这在传统的大规模 JS 应用里面是很难得的。以前总感觉 JS 一复杂,就感觉质量难以保证,线上运行也有点虚,说不定什么时候就爆出一个 's' is undefined, 'b' is not a function 之类的错误。现在有了静态检查,心里更有底了

增强设计阶段

上面章节已经就 TS 对面向对象设计能力的增强做了描述。设计的增强,是可以提升代码质量的。良好的设计面对需求迭代的不断冲击,可以保持代码的可维护性和可扩展性,也就提升了代码的质量和健康度。

总结

现在的 Web 应用很多都是复杂的单页应用,尤其是一些工具类的产品,复杂度慢慢接近甚至不亚于一些传统的桌面软件,如 Word、Excel、SQL 客户端、数据集成分析工具等等,这些 Web 应用都可以说是大规模 JS 应用,需要一个团队来协作迭代开发。那么协作效率、代码可读性、可维护性、健壮性等等都是我们应该重点专注的点。

协作效率本身比较难以衡量,但可以从阅读、维护别人代码的难度和效率这一点来说明。如果有有完善的类型定义,再加上智能的 TS IDE(如 VS Code),从我们项目的实践经验来看可以显著的提升代码的可读性、可维护性。很多时候都是看看源代码就能较容易的发现一些问题和 Bug。随着产品的不断迭代,一来一回之间就提升了整个团队的协作效率。

我们项目的 TS 校验规则是很严格的,意味着代码中基本没有 any ,都需要定义具体类型。对于一些公共的模块,我们也会严格定义 private、protected、public 等访问控制符,这意味着代码即文档,代码就能说明哪些是对外的接口,哪些是内部使用的。

一些常用的第三方库和框架都有完善的 TS 类型定义,同时整个项目业务代码都有完善的 TS 类型定义,静态检查出错的都会在 IDE 中提示出来,甚至可以阻断构建流程,这样可以减少 Bug 的产生,代码质量更为可控,代码健康度更高,心里也更有底了。

相关资源

对我们团队感兴趣的可以关注专栏或者发送简历至'tao.qit####alibaba-inc.com'.replace('####', '@'),欢迎有志之士加入~

原文地址:github.com/ProtoTeam/b…

关注下面的标签,发现更多相似文章
评论

查看更多 >