2天时间,聊今年最热的 Agent、上下文工程、AI 产品创新等话题。2025 年最后一场~ 了解详情
写点什么

编写高质量可维护的代码: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:003193

评论 1 条评论

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

原创 | 使用JPA实现DDD持久化-启动JPA程序+通过JPA原生API访问数据

编程道与术

Java hibernate 编程 mybatis jpa

Java程序员还没有掌握SpringBoot?这一份文档你真应该好好学学!

Java架构之路

Java 程序员 架构 面试 编程语言

读《全球创新投资:经济大变局中的财富新机遇》

邓瑞恒Ryan

创业 读书笔记 企业 企业管理 产业竞争

原创 | 使用JPA实现DDD持久化-O/R映射元数据-值和关联的比较+继承映射

编程道与术

Java hibernate 编程 mybatis jpa

架构师训练营 1 期 -- 第十三周作业

曾彪彪

极客大学架构师训练营

架构师训练营第四周命题作业

Geek_xq

架构师训练营第 1 期 -week13

习习

朱嘉明:产业周期、科技周期与金融周期的失衡

CECBC

金融 科技

JavaScript中,if判断未生效的一些特殊情况

Geek_de9857

js 1 if 0 不生效

阿里Java研发第2面,都会问什么?斩获阿里P6+的“他”告诉你答案

Java架构之路

Java 程序员 架构 面试 编程语言

第九周作业

Griffenliu

阿里Java岗面试必备JVM指南:内存分配+垃圾回收+调优+类加载器等

Java架构之路

Java 程序员 架构 面试 编程语言

原创 | 使用JPA实现DDD持久化-通过Spring Data JPA访问数据

编程道与术

Java hibernate 编程 mybatis jpa

一周信创舆情观察(12.7~12.13)

统小信uos

盘点 2020 | 感悟总结

idonkeyliu

盘点2020

15年华为云视频架构师采访实录:揭秘未来音视频行业的科技趋势!

华为云开发者联盟

直播 视频 华为云

一文详解激活函数

书豪

阿里云容器服务入选云原生边缘「领导力企业TOP3」,推动「原生云边」基础设施标准建立

阿里巴巴云原生

阿里云 容器 开发者 云原生 边缘计算

Hive中,同时存在map、array、struct这三种格式,应如何在建表语句中指定分隔符?

Geek_de9857

hive struct map array 分隔符

终于有人把性能优化讲清楚了!阿里架构师推荐的Java性能权威指南可太强了

Java架构之路

Java 程序员 架构 面试 编程语言

滴滴DoKit阶段性成果汇报之一机多控

工具 滴滴开源 DoKit

原创 | 使用JPA实现DDD持久化-O/R映射元数据-关联属性映射

编程道与术

Java hibernate 编程 mybatis jpa

佛萨奇Forsage系统开发,智能合约dapp技术

薇電13242772558

智能合约 dapp

【涂鸦物联网足迹】用煲仔饭来说明IaaS/PaaS/SaaS的区别

IoT云工坊

云计算 IaaS PaaS SaaS 云平台

eCharts -- 如何修改柱状图中相关数据的顺序?如何在鼠标悬浮时增加百分比信息?如何为柱状图设置分组?

Geek_de9857

柱状图 eCharts 修改数据顺序 百分比提示信息 设置分组

第九周学习总结

Griffenliu

自从看了百度强推的“Spring源码笔记”我从渣渣成功逆袭成为钢铁侠!

比伯

Java 架构 程序人生 计算机 spring源码

基于区块链的政务平台设计探索

华为云开发者联盟

区块链 架构 安全

Mysql中,1=1和 1=1=1 和 -1=-1 和 -1=-1=-1 和 5=5 和 5=5=5 有什么区别

Geek_de9857

MySQL sql 返回值 1=1=1 -1=1=1

去 DeepMind 面试是怎样一种体验?

故胤道长

人工智能 面试 谷歌Google 硅谷 移动开发

原创 | 使用JPA实现DDD持久化-O/R映射元数据-值属性映射

编程道与术

Java hibernate 编程 mybatis jpa

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