乐趣区

关于前端:编写高质量可维护的代码Awesome-TypeScript

这是第 84 篇不掺水的原创,想获取更多原创好文,请搜寻公众号关注咱们吧~ 本文首发于政采云前端博客:编写高质量可保护的代码: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); // momo
    person.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) // JS
if(result?.data?.list) // TS
  • 联结判断是否为空值,能够应用 ??
let temp = (val !== null && val !== void 0 ? val : '1'); // JS
let 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;
}
  • 应用 Enum 保护常量表,实现更平安的类型查看
// 应用 const enum 保护常量
const enum PROJ_STATUS {
  PENDING = 'PENDING',
  PROCESS = 'PROCESS',
  COMPLETED = 'COMPLETED'
}
function handleProject (status: PROJ_STATUS): void {
}
handleProject(PROJ_STATUS.COMPLETED)
  • 倡议开启以下编译查看选项,便于在编译环境发现潜在 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 语句贯通)}
}

相干 VSCode 插件举荐

TypeScript Extension Pack,它汇合了咱们日常罕用的 TypeScript 相干插件:

  • TSLint:自动检测和修复不符合规范的 TypeScript 代码。
  • TypeScript Hero:对 import 引入模块程序进行排序和组织,移除未被应用的。MacOS 上快捷键 Ctrl+Opt+o,Win/Linux 上快捷键 Ctrl+Alt+o
  • json2ts:将剪切板中的 JSON 转化成 TypeScript 接口。MacOS 上快捷键 Ctrl+Opt+V,Win/Linux 上快捷键 Ctrl+Alt+V
  • Move TS:在挪动 TypeScript 文件或者蕴含 TypeScript 文件的文件夹时,会自动更新相干依赖模块的 import 门路。
  • Path Intellisense:门路和文件名的主动提醒补全性能。
  • TypeScript Importer:import 引入模块时,主动搜寻以后 workspace 下所有 export 的模块,并主动进行提醒补全。
  • Prettier – Code formatter:格式化代码。

招贤纳士

政采云前端团队(ZooTeam),一个年老富裕激情和创造力的前端团队,隶属于政采云产品研发部,Base 在风景如画的杭州。团队现有 40 余个前端小伙伴,平均年龄 27 岁,近 3 成是全栈工程师,妥妥的青年风暴团。成员形成既有来自于阿里、网易的“老”兵,也有浙大、中科大、杭电等校的应届新人。团队在日常的业务对接之外,还在物料体系、工程平台、搭建平台、性能体验、云端利用、数据分析及可视化等方向进行技术摸索和实战,推动并落地了一系列的外部技术产品,继续摸索前端技术体系的新边界。

如果你想扭转始终被事折腾,心愿开始能折腾事;如果你想扭转始终被告诫须要多些想法,却无从破局;如果你想扭转你有能力去做成那个后果,却不须要你;如果你想扭转你想做成的事须要一个团队去撑持,但没你带人的地位;如果你想扭转既定的节奏,将会是“5 年工作工夫 3 年工作教训”;如果你想扭转原本悟性不错,但总是有那一层窗户纸的含糊… 如果你置信置信的力量,置信平凡人能成就不凡事,置信能遇到更好的本人。如果你心愿参加到随着业务腾飞的过程,亲手推动一个有着深刻的业务了解、欠缺的技术体系、技术发明价值、影响力外溢的前端团队的成长历程,我感觉咱们该聊聊。任何工夫,等着你写点什么,发给 ZooTeam@cai-inc.com

退出移动版