写点什么

编写高质量可维护的代码:Awesome TypeScript

  • 2021-05-31
  • 本文字数:5638 字

    阅读完需:约 18 分钟

编写高质量可维护的代码:Awesome TypeScript

前言


高质量可维护的代码应具备可读性高、结构清晰、低耦合、易扩展等特点。而原生的 JavaScript 由于其弱类型和没有模块化的缺点,不利于大型应用的开发和维护,因此,TypeScript 也就应运而生。


TypeScript 是 JavaScript 的一个超集,它的设计初衷并不是为了替代 JavaScript,而是基于 JavaScript 做了一系列的增强,包括增加了静态类型、接口、类、泛型、方法重载等等。所以,只要你有一定的 JavaScript 功底,那么 TypeScript 上手就非常简单。并且,你可以在 TypeScript 中愉快的使用 JavaScript 语法。


接下去,本文将给大家分享下,TypeScript 的重要特性以及在实际场景中的使用技巧,帮助大家更高效的编写高质量可维护的代码。


Typescript VS Javascript


JavaScript


  • JavaScript 是动态类型语言,在代码编译阶段不会对变量进行类型检测,从而会把潜在的类型错误带到代码执行阶段。并且在遇到不同类型变量的赋值时,会自动进行类型转换,带来了不确定性,容易产生 Bug。

  • JavaScript 原生没有命名空间,需要手动创建命名空间,来进行模块化。并且,JavaScript 允许同名函数的重复定义,后面的定义可以覆盖前面的定义。这也给我们开发和维护大型应用带来了不便。


TypeScript


  • TypeScript 是静态类型语言,通过类型注解提供编译时的静态类型检查。

  • 在代码编译阶段会进行变量的类型检测,提前暴露潜在的类型错误问题。并且在代码执行阶段,不允许不同类型变量之间的赋值。

  • 清晰的类型注解,不仅让代码的可读性更好,同时也增强了 IDE 的能力,包括代码补全、接口提示、跳转到定义等等。

  • TypeScript 增加了模块类型,自带命名空间,方便了大型应用的模块化开发。

  • TypeScript 的设计一种完全面向对象的编程语言,具备模块、接口、类、类型注解等,可以让我们的代码组织结构更清晰。


经过上述对比,可以看到 TypeScript 的出现很好的弥补了 JavaScript 的部分设计缺陷,给我们带来了很大的便利,也提高了代码的健壮性和扩展性。


重要特性


数据类型


  • 基础数据类型包括:Boolean、Number、String、Array、Enum、Any、Unknown、Tuple、Void、Null、Undefined、Never。下面选择几个 TypeScript 特有的类型进行详解:

  • Enum 枚举:在编码过程中,要避免使用硬编码,如果某个常量是可以被一一列举出来的,那么就建议使用枚举类型来定义,可以让代码更易维护。


  // 包括 数字枚举、字符串枚举、异构枚举(数字和字符串的混合)。  // 数字枚举在不设置默认值的情况下,默认第一个值为0,其他依次自增长  enum STATUS {    PENDING,    PROCESS,    COMPLETED,  }  let status: STATUS = STATUS.PENDING;  // 0
复制代码


  • Any 类型:不建议使用。Any 类型为顶层类型,所有类型都可以被视为 any 类型,使用 Any 也就等同于让 TypeScript 的类型校验机制失效。

  • Unknown 类型:Unknown 类型也是顶层类型,它可以接收任何类型,但它与 Any 的区别在于,它首次赋值后就确定了数据类型,不允许变量的数据类型进行二次变更。所以,在需要接收所有类型的场景下,优先考虑用 Unknown 代替 Any。

  • Tuple 元组:支持数组内存储不同数据类型的元素,让我们在组织数据的时候更灵活。


let tupleType: [string, boolean];tupleType = ["momo", true];
复制代码


  • Void 类型:当函数没有返回值的场景下,通常将函数的返回值类型设置为 void。


类型注解


  • TypeScript 通过类型注解提供编译时的静态类型检查,可以在编译阶段就发现潜在 Bug,同时让编码过程中的提示也更智能。使用方式很简单,在 : 冒号后面注明变量的类型即可。

const str: string = 'abc';
复制代码


接口


  • 在面向对象编程的语言里面,接口是实现程序解耦的关键,它只定义具体包含哪些属性和方法,而不涉及任何具体的实现细节。接口是基于类之上,更进一步对实体或行为进行抽象,会让程序具备更好的扩展性。

  • 应用场景:比如我们在实现订单相关功能的时候,需要对订单进行抽象,定义一个订单的接口,包括订单基本信息以及对订单的相关操作,然后基于这个接口来做进一步的实现。后续如果订单的相关操作功能有变化,只需要重新定义一个类来实现这个接口即可。

interface Animal {name: string;getName(): string;}class Monkey implements Padder {constructor(private name: string) {  getName() {    return 'Monkey: ' + name; }}}
复制代码



  • TypeScript 的类除了包括最基本的属性和方法、getter 和 setter、继承等特性,还新增了私有字段。私有字段不能在包含的类之外访问,甚至不能被检测到。Javascript 的类中是没有私有字段的,如果想模拟私有字段的话,必须要用闭包来模拟。下面用一些示例来说明下类的使用:

  • 属性和方法

class Person {// 静态属性static name: string = "momo";// 成员属性gender: string;// 构造函数constructor(str: string) {  this.gender = str;}// 静态方法static getName() {  return this.name;}// 成员方法getGender() {  return 'Gender: ' + this.gender;}}let person = new Person("female");
复制代码


  • getter 和 setter

  • 通过 getter 和 setter 方法来实现数据的封装和有效性校验,防止出现异常数据。

class Person {private _name: string;get name(): string {  return this._name;}set name(newName: string) {  this._name = newName;}}let person = new Person('momo');console.log(person.name); // momoperson.name = 'new_momo';console.log(person.name); // new_momo
复制代码


  • 继承

class Animal {name: string;constructor(nameStr=:string) {  this.name = nameStr;}  move(distanceInMeters: number = 0) {  console.log(`${this.name} moved ${distanceInMeters}m.`);}}class Snake extends Animal {constructor(name: string) {  super(name);move(distanceInMeters = 5) {  super.move(distanceInMeters);}}let snake = new Snake('snake');snake.move(); // 输出:'snake moved 5m'
复制代码


  • 私有字段

  • 私有字段以 # 字符开头。私有字段不能在包含的类之外访问,甚至不能被检测到。

class Person {#name: string;constructor(name: string) {  this.#name = name;}greet() {  console.log(`Hello, ${this.#name}!`);}}let person = new Person('momo');person.#name;   // 访问会报错
复制代码


泛型


  • 应用场景:当我们需要考虑代码的可复用性时,就需要用到泛型。让组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型。泛型允许同一个函数接受不同类型参数,相比于使用 Any 类型,使用泛型来创建的组件可复用和易扩展性要更好,因为泛型会保留参数类型。泛型可以应用于接口、类、变量。下面用一些示例来说明下泛型的使用:

  • 泛型接口

  interface identityFn<T> {    (arg: T): T;  }
复制代码
  • 泛型类

  class GenericNumber<T> {    zeroValue: T;    add: (x: T, y: T) => T;  }  let myGenericNumber = new GenericNumber<number>();  myGenericNumber.zeroValue = 0;  myGenericNumber.add = function (x, y) {    return x + y;  };
复制代码
  • 泛型变量

    使用大写字母 A-Z 定义的类型变量都属于泛型,常见泛型变量如下:

  • T(Type):表示一个 TypeScript 类型

  • K(Key):表示对象中的键类型

  • V(Value):表示对象中的值类型

  • E(Element):表示元素类型


交叉类型


  • 交叉类型就是将多个类型合并为一个类型。通过 & 运算符定义。如下示例中,将 Person 类型和 Company 类型合并后,生成了新的类型 Staff,该类型同时具备这两种类型的所有成员。


interface Person {name: string;gender: string;}interface Company {companyName: string;}type Staff = Person & Company;const staff: Staff = {name: 'momo',gender: 'female',companyName: 'ZCY'};
复制代码


联合类型


  • 联合类型就是由具有或关系的多个类型组合而成,只要满足其中一个类型即可。通过 | 运算符定义。如下示例中,函数的入参为 String 或 Number 类型即可。

function fn(param: string | number): void {  console.log("This is the union type");}
复制代码


类型保护


类型保护就是在我们已经识别到当前数据是某种数据类型的情况下,安全的调用这个数据类型对应的属性和方法。常用的类型保护包括 in 类型保护、typeof 类型保护、instanceof 类型保护和 自定义 类型保护。具体见以下示例:


  • in 类型保护

  interface Person {    name: string;    gender: string;  }  interface Employee {    name: string;    company: string;  }  type UnknownStaff = Person | Employee;  function getInfo(staff: UnknownStaff) {    if ("gender" in staff) {      console.log("Person info");    }    if ("company" in staff) {      console.log("Employee info");    }  }
复制代码
  • typeof 类型保护

  function processData(param: string | number): unknown {   if (typeof param === 'string') {     return param.toUpperCase()    }    return param;  }
复制代码
  • instanceof 类型保护:和 typeof 类型用法相似,它主要是用来判断是否是一个类的对象或者继承对象的。

  function processData(param: Date | RegExp): unknown {   if (param instanceof Date) {     return param.getTime();    }    return param;  }
复制代码


  • 自定义 类型保护:通过类型谓词 parameterName is Type 来实现自定义类型保护。如下示例,实现了接口的请求参数的类型保护。


  interface ReqParams {   url: string;    onSuccess?: () => void;    onError?: () => void;  }  // 检测 request 对象包含参数符合要求的情况下,才返回 url  function validReqParams(request: unknown): request is ReqParams {   return request && request.url  }
复制代码


开发小技巧


  • 需要连续判断某个对象里面是否存在某个深层次的属性,可以使用 ?.

if(result && result.data && result.data.list) // JSif(result?.data?.list) // TS
复制代码
  • 联合判断是否为空值,可以使用 ??

let temp = (val !== null && val !== void 0 ? val : '1'); // JSlet temp = val ?? '1'; // TS
复制代码


  • 不要完全依赖于类型检查,必要时还是需要编写兜底的防御性代码。

  • 因为类型报错不会影响代码生成和执行,所以原则上还是会存在 fn('str') 调用的可能性,所以需要 default 进行兜底的防御性代码。


function fn(value:boolean){ switch(value){   case true:      console.log('true');      break;    case false:       console.log('false');      break;    default:       console.log('dead code');  }}
复制代码


  • 对于函数,要严格控制返回值的类型.

// 推荐写法function getLocalStorage<T>(key: string): T | null {  const str = window.localStorage.getItem(key);  return str ? JSON.parse(str) : null;}const data = getLocalStorage<DataType>("USER_KEY");
复制代码


  • 利用 new() 实现工厂模式

  • TypeScript 语法实现工厂模式很简单,只需先定义一个函数,并声明一个构造函数的类型参数,然后在函数体里面返回 c 这个类构造出来的对象即可。以下示例中,工厂函数构造出来的是 T 类型的对象。

function create<T>(c: { new(): T }): T { return new c();}class Test {  constructor() {  }}create(Test);
复制代码


  • 优先考虑使用 Unknown 类型而非 Any

  • 使用 readonly 标记入参,保证参数不会在函数内被修改

function fn(arr:readonly number[] ){  let sum=0, num = 0;  while((num = arr.pop()) !== undefined){    sum += num;  }  return sum;}
复制代码


  • 优先考虑使用 Unknown 类型而非 Any

  • 使用 readonly 标记入参,保证参数不会在函数内被修改

function fn(arr:readonly number[] ){  let sum=0, num = 0;  while((num = arr.pop()) !== undefined){    sum += num;  }  return sum;}
复制代码


  • 建议开启以下编译检查选项,便于在编译环境发现潜在 Bug


{  "compilerOptions": {    /* 严格的类型检查选项 */    "strict": true,                    // 启用所有严格类型检查选项    "noImplicitAny": true,             // 在表达式和声明上有隐含的 any类型时报错    "strictNullChecks": true,          // 启用严格的 null 检查    "noImplicitThis": true,            // 当 this 表达式值为 any 类型的时候,生成一个错误    "alwaysStrict": true,              // 以严格模式检查每个模块,并在每个文件里加入 'use strict'        /* 额外的检查 */    "noUnusedLocals": true,            // 有未使用的变量时,抛出错误    "noUnusedParameters": true,        // 有未使用的参数时,抛出错误    "noImplicitReturns": true,         // 并不是所有函数里的代码都有返回值时,抛出错误    "noFallthroughCasesInSwitch": true,// 报告 switch 语句的 fallthrough 错误。(即,不允许 switch 的 case 语句贯穿)  }}
复制代码



头图:Unsplash

作者:沫沫

原文:https://mp.weixin.qq.com/s/gAwvcmSNYMwQKk6RY-GaEw

原文:编写高质量可维护的代码:Awesome TypeScript

来源:政采云前端团队 - 微信公众号 [ID:Zoo-Team]

转载:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

2021-05-31 16:002403

评论 1 条评论

发布
用户头像
以下内容重复了:
```
优先考虑使用 Unknown 类型而非 Any
使用 readonly 标记入参,保证参数不会在函数内被修改
```
2021-06-01 08:02
回复
没有更多了
发现更多内容

福昕软件与神州数码达成中国区分销合作

联营汇聚

周日直播,龙蜥社区等8位专家在线报告 | 2022大学生操作系统大赛培训会

OpenAnolis小助手

操作系统 人才培养 大学生 龙蜥社区

干货| 四种渲染到底是啥?终于有人讲明白了!(上)

Orillusion

开源 WebGL 元宇宙 Metaverse webgpu

学生管理系统(2)

5月月更

最「难搞」的英伟达也开源了,苹果会是下一个吗?

腾源会

开源 英伟达 苹果 腾源会

如何设计产品帮助中心?从这几点出发

小炮

看 AWS 如何通过 Nitro System 构建竞争优势

云物互联

云计算 AWS DPU 硬件虚拟化技术

从相亲来看Flutter 的 StatefulWidget 和 StatelessWidget

岛上码农

flutter ios开发 安卓开发 跨平台应用 5月月更

企评家,为沪深主板企业成长性评价

企评家

不用PyScript,网页端运行的Python编辑器

Alexads

Python 代码编辑器

【愚公系列】2022年05月 二十三种设计模式(十三)-职责链模式(Chain of Responsibility Pattern)

愚公搬代码

5月月更

使用声网 SDK 为Android APP添加视频直播

声网

android 教程 视频直播

Redis「2」缓存一致性与异常处理

Samson

Redis 核心技术与实战 学习成长 5月月更

即时通讯安全篇(九):为什么要用HTTPS?深入浅出,探密短连接的安全性

JackJiang

https 即时通讯 ssl im开发

面向CV编程:COPY了别人文章中的代码,想让代码能像作者一样跑通,应该注意什么呢?怎样才能让代码愉快地跑起来呢

迷彩

读书笔记 高效工作 程序员 个人思考 5月月更

【LeetCode】一次编辑Java题解

Albert

LeetCode 5月月更

druid 源码阅读 4——走一个单测看sql运行

张大彪

Druid连接池源码阅读04

石小天

企评家 | 每日互动股份有限公司成长性评价简介

企评家

覆盖 70% 核心业务,ShardingSphere 如何成为喜马拉雅架构演进的催化剂

SphereEx

Apache 数据库 开源 ShardingSphere SphereEx

Collections和Objects的使用注意

zarmnosaj

5月月更

[数据分析实践]-音频分析-BirdCLE-2

浩波的笔记

人工智能 机器学习 数据分析

druid 源码阅读(四)返回一个连接

爱晒太阳的大白

5月月更

druid源码学习四-多线程之锁探究

Nick

Apache Druid 锁机制 多线程安全

数据库连接池 -Druid 源码学习(四)

wjchenge

Druid 数据库连接池

Maven 项目

Emperor_LawD

maven 5月月更

Cocos Creator学习の有限状态机

空城机

Cocos 5月月更

前缀和算法

工程师日月

算法 5月月更

一份让面试官满意的简历究竟要做到什么

宇宙之一粟

简历模板 简历 5月月更

使用 Amazon Cloud WAN 构建您的全球网络

云物互联

云计算 AWS Cloud WAN

设计模式之原型模式

乌龟哥哥

5月月更

编写高质量可维护的代码:Awesome TypeScript_语言 & 开发_政采云前端团队_InfoQ精选文章