美文网首页js css html
如何在项目中用好TypeScript

如何在项目中用好TypeScript

作者: 没名字的某某人 | 来源:发表于2022-03-21 11:37 被阅读0次

    写在最前:本文转自掘金

    1. 前言

    • 我们都知,JavaScript 是一门非常灵活的编程语言,这种灵活也使他的代码质量参差不齐,维护成本高,运行时错误多。
    • TypeScript是添加了类型系统的JavaScript,适用于任何规模的项目,TS的类型系统很大程度上弥补了JS的缺点。
    • 类型系统按照[类型检查的时机]来分类,可以分为动态类型和静态类型:
      · 动态类型是指运行时才会进行类型检查,这种语言的类型错误往往会导致运行时的错误,JS属于动态类型,它是一门解释型语言,没有编译阶段。
      · 静态类型是只编译阶段就能确定每个变量的类型,这种语言的类型错误往往会导致语法错误。由于TS在运行前需要先编译成JS,而在编译阶段就会进行类型检查,所以TS属于静态类型。
    • TS增强了编辑器的功能,包括代码补全、接口提示、跳转到定义、代码重构等,这在很大程度上提高了开发效率。TS的类型系统可以为大型项目带来更高的可维护性,以及更少的bug。
    • 为了提升开发幸福感,下面将详细介绍如何在项目中用好TS。

    2. 在项目中的实践

    2.1 善用类型注释

    • 我们可以通过/** */ 形式的注释给TS类型做标记提示:
    /** person information**/
    interface User{
      name: string;
      age: number;
      sex: 'male' | 'female' ;
    }
    
    const p:User = {
      name: "Lucky",
      age:20,
      sex:"female"
    }
    

    当鼠标悬浮在使用到该类型的地方时,编辑器会有更好的提示:


    test1.png

    2.2 善用类型扩展

    • TS 中定义类型有两种方式:接口(interface)和类型别名(type alias)。在下面的例子中,除了语法不一样,定义的类型是一样的:
    // interface
    interface Point{
      x: number;
      y: number;
    }
    interface SetPoint{
      (x: number, y: number): void;
    }
    
    // type
    type Point = {
      x: number;
      y: number;
    }
    type SetPoint = (x: number, y: number)=> void;
    
    • 接口和类型别名均可以扩展:
    // Interface extends interface
    interface PartialPointX{
      x: number;
    }
    interface Point extends PartialPointX{
      y: number;
    }
    
    // Type alias extends type alias
    type PartialPointX={
      x:number;
    }
    type Point = PartialPointX & {y:number;};
    
    • 接口和类型别名并不互斥的,也就是说,接口可以扩展类型别名,类型别名也可以扩展接口:
    // Interface extends type alias
    type PartialPointX={
      x: number;
    }
    interface Point extends PartialPointX{
      y: number;
    }
    
    // Type alias extends interface
    interface PartialPointX{
      x:number;
    }
    type Point = PartialPointX & {y:number;};
    
    • 接口和类型别名的选用时机
      · 在定义公共API(如编辑一个库)时使用interface,这样可以方便使用者继承接口;
      · 在定义组件属性(Props)和状态(State)时,建议使用type,因为type的约束性更强;
      · type类型不能二次编辑,而interface可以随时扩展。

    2.3 善用声明文件

    • 声明文件必须以.d.ts为后缀。一般来说,TS会解析项目中所有的*.ts文件,因此也包含以.d.ts结尾的声明文件。
    • 只要ts.config.json中的配置包含了typing.d.ts文件,那么其他的所有*.ts文件就都可以获得声明文件的类型定义。
    2.3.1 第三方声明文件
    • 当在TS项目中使用第三方库时,我们需要引用它的声明文件,才能获得对应的代码补全、接口提示等功能。
    • 针对多数第三方库,社区已经帮我们定义好了它们的声明文件,我们可以直接下载下来使用。一般推荐使用@types 统一管理第三方库的声明文件,@types的使用非常简单,直接用npmyarn安装对应的声明模块即可。以lodash为例:
    npm install @types/lodash --save-dev
    // or
    yarn add @types/lodash --dev
    
    2.3.2 自定义声明文件
    • 当一个库没有提供声明文件,就需要我们自己写声明文件,以antd-dayjs-webpack-plugin为例,当在config.ts中使用antd-dayjs-webpack-plugin时,若当编辑器没有找到它的声明文件,则会发生报错
    • 为了解决编辑器的报错提示,我们可以采用它提供的另一种方法:添加一个包含 declare module 'antd-dayjs-webpack-plugin'; 的新声明文件。我们也可以不用新增文件,在前面提到的 typing.d.ts 添加下面的内容即可:
    declare module 'antd-dayjs-webpack-plugin';
    

    全局变量

    当我们需要在多个ts文件中使用同一个typescript类型时,常见做法会在constant.ts文件中声明相关类型,并将其export出去给其他ts文件import使用,无疑会产生很多繁琐的代码。前面我们提到,只要在tsconfig.json中配置包含了我们自定义的声明文件*.d.ts,则声明文件中的类型都能被项目中.ts文件获取到。因此我们可以将多个ts文件都需要使用的全局类型卸载声明文件中,需要使用该类型的ts文件不需要import就可以直接使用。

    命名空间

    在代码量较大的情况下,为了避免各种变量名冲突,可将相同模块的函数、类、接口等放置在命名空间内。


    test2.jpeg

    在ts文件使用:

    // src/views/Domain/index.ts
    const cloumns: Domains.ListItem[] = []
    ...
    
    // src/views/Department/index.ts
    const columns: Departments.ListItem[] = []
    

    2.4 善用 TypeScript 支持的JS新特性

    2.4.1 可选链
    let  age = user && user.info && user.info.getAge  // 写法冗余且容易命中 `Uncaught TypeError: Cannot read property ...`
    
    let age = user?.info?.getAge  //如果其中有属性不存在,会返回`null`或者`undefined`
    
    2.4.2 空值合并运算符

    当左侧的操作数为null或者undefined时,返回其右侧操作数,否则返回左侧操作数。

    const user = {
      level: 0, 
    }
    let level1 = user.level ?? '暂无等级'  // 0
    let level2 = user.other_level ?? ''暂无等级'  // 暂无等级
    

    ||不同,或操作符为false值(例如,' '0)时返回右侧操作数

    2.5 善用访问限定

    TS的类定义时允许使用privateprotectedpublic三种访问修饰符声明成员访问限制,并在编译期间检查,如果不加任何修饰符,默认为public访问级别:

    class Person {
      private name: string;
      private age: number;
      // static 关键字可以将类里面的属性和方法定义为类的静态属性和方法
      public static sex: string = 'Male';
      constructor(name: string, age: number){
        this.name = name;
        this.age = age;
      }
      public run(): void {
        console.log(this.name + '在跑步')
      }
      public setName(name:string): void {
        this.name = name;
      }
    }
    let p: Person = new Person('Tony', 22);
    console.log(Person.sex);  // Male
    p.run();  //Tony在跑步
    console.log(p.name) // name为私有属性
    

    2.6 善用类型收窄

    TypeScript 类型收窄就是从宽类型转换成窄类型的过程,其常用于处理联合类型变量的场景。主要有以下方法收窄变量类型:

    • 类型断言
    • 类型守卫
    • 双重断言
    2.6.1 类型断言
    • 类型断言可以明确地告诉TS值的详细类型。其语法如下:
    值 as 类型
    // or
    <类型>值
    
    • 在 tsx 语法中必须使用前者
    • 当TS不确定一个联合类型的变量到底是哪个类型的时候,我们只能访问此联合类型的所有类型中共有的属性和方法。
    • 需要注意的是,类型断言只能够欺骗TS编译器,无法避免运行时的错误,反而滥用类型断言可能会导致运行时错误
    2.6.2 类型守卫

    类型守卫主要有以下几种方式:

    • typeof :用于判断numberstringbooleansymbol四种类型;
    • instanceof:用于判断一个实例是否属于某个类;
    • in:用于判断一个属性/方法是否属于某个对象

    typeof

    可以利用 typeof 实现类型收窄和never 类型的特性做全面性检查,如下面代码所示:

    type Foo = string | number
    
    function controlFlowAnalysisWithNever(foo: Foo){
      if(typeof foo === 'string'){
        // 这里foo收窄为 string类型
      }else if (typeof foo === 'number'){
        // 这里foo 被收窄为number类型
      } else {
        // foo 在这里是never
        const check: never = foo;
      }
    }
    

    可以看到,在最后的else分支里面,我们把收窄为neverfoo赋值给一个显式声明的never变量,如果一切逻辑正确,那么是能够编译通过。假如某天某人修改了Foo的类型,而忘记修改controlFlowAnalysisWithNever方法中的控制流程,这时候else分支的foo类型无法赋值给never类型,产生一个编译错误。通过使用never避免出现新增了联合类型没有对应的实现,确保了方法总是穷尽Foo的所有类型,从而保证代码的安全性。

    instanceof

    使用instanceof运算符收窄变量的类型:

    class Man {
      handsome = "handsome";
    }
    class Women{
      beautiful = "beautiful"
    }
    
    function Human(arg: Man | Woman){
      if(arg instanceof Man){
        console.log(arg.handsome)
      }else{
        console.log(arg.beautiful)
      }
    }
    

    in

    使用in做属性检查

    interface A{
      a: string; 
    }
    interface B{
      b: string; 
    }
    function foo(x: A | B){
      if("a" in x){
        return x.a;
      }
      return x.b
    }
    
    2.6.3 双重断言

    当我们要为某个值作类型断言时,我们需要确保编辑器推断出的值的类型和新类型有重叠,否则,无法简单地作类型断言,任何类型都可以被断言为any,而any可以被断言为任何类型
    如果我们仍然想使用那个类型,可以使用双重断言

    function handler(event: Event){
      const element = event as any as HTMLElement;
    }
    

    TS3.0中新增了一种unknown类型,它是一种更加安全的any的副本。所有东西都可以被标记成是unknown类型,但是unkonwn必须在进行类型判断和条件控制之后才可以被其他类型,并且在类型判断和条件控制之前也不能进行任何操作

    2.7 善用常量枚举

    2.8 善用高级类型

    除了stringnumberboolean这种基础类型外,我们还应该了解一些类型声明中的一些高级用法。

    2.8.1 类型索引(keyof)

    keyof 类似于Object.keys,用于获取一个接口中key的联合类型:

    interface Button{
      type: string;
      text: string;
    }
    type ButtonKeys = keyof Button
    // 等效于
    type ButtonKeys = "type" | "text"
    
    2.8.2 类型约束(extends)

    TSextends关键词不同于在Class后使用extends的继承作用,一般在泛型内使用,它主要作用是对泛型加以约束:

    type BaseType = string | number | boolean  // 这里表示copy 的参数
    
    function copy<T extends BaseType>(arg: T):T{
      return arg
    }
    const arr = copy([])  // error
    

    extends经常和keyof一起使用,例如我们有一个getValue方法专门获取对象的值,但是这个对象并不确定,我们就可以这样做:

    function getValue<T, K extends keyof T>(obj: T, key: K){
      return obj[key]
    }
    
    const obj = {a:1}
    const a = getValue(obj, 'b')  //error
    

    当传入对象没有key时,编辑器则会报错

    2.8.3 类型映射(in)

    in关键词的作用主要是做类型的映射,遍历已有接口的key或者是遍历联合类型。以内置的泛型接口Readonly为例,它的实现如下:

    type Readonly<T> ={
      readonly [P in keyof T]: T[P];
    } 
    // 它的作用是将所有接口变为只读
    interface Obj {
      a: string;
      b: string;
    }
    type ReadOnlyObj = Readonly<Obj>
    //等效于
    interface Obj {
      readonly a: string;
      readonly b: string;
    }
    
    2.8.3 条件类型(U?X:Y)

    条件类型的语法规则和三元表达式一致,经常用于类型不确定的情况

    T extends U ? X : Y
    

    上面的意思就是,如果 T 是 U 的子集,就是类型 X,否则为类型 Y。以内置的泛型接口 Extract 为例,它的实现如下:

    type Extract<T,U> = T extends U ? T : never;
    

    TypeScript 将使用 never类型来表示不应该存在的状态。上面的意思是,如果 T 中的类型在 U 存在,则返回,否则抛弃。
    假设我们两个类,有三个公共的属性,可以通过Extract提取这三个公共属性:

    interface Worker{
      name: string;
      age: number;
      email:string;
      salary: number;
    }
    interface Worker{
      name: string;
      age: number;
      email:string;
      grade: number;
    }
    
    type CommonKeys = Extract<keyof Worker, keyof Student>
    // 'name' | 'age' | 'email'
    
    2.8.4工具泛型

    TS 中内置了很多工具泛型,前面介绍过Readonly、'Extract' 这两种,内置的泛型在TS内置的lib.es5.d.ts中都有定义,所以不需要任何依赖就可以直接使用。下面介绍几个常见的工具泛型的作用和使用方法。
    Exclude,如果T中的类型在U不存爱,则返回,否则抛弃。

    interface Worker{
      name: string;
      age: number;
      email:string;
      salary: number;
    }
    interface Worker{
      name: string;
      age: number;
      email:string;
      grade: number;
    }
    
    type CommonKeys = Exclude<keyof Worker, keyof Student>
    // 'salary' 
    

    Partial 用于将一个接口所有属性设置为可选状态:

    interface Person  {
      name: string;
      sex: string;
    }
    type NewPerson = Partial<Person>
    // {name?:string;sex?:string}
    

    Required的作用是将所有接口可选属性改为必选的

    interface Person  {
      name?: string;
      sex?: string;
    }
    type NewPerson = Required<Person>
    // {name:string;sex:string}
    

    Pick主要作用提取接口的某几个属性:

    interface Todo{
      title: string;
      completed: boolean;
      description: string
    }
    type TodePrevied = Pick<Tode, "title"|"completed">
    // {title: string;completed:boolean}
    

    Omit的作用是剔除接口的某几个属性

    interface Todo{
      title: string;
      completed: boolean;
      description: string
    }
    type TodePrevied = Omit<Tode, "description">
    // {title: string;completed:boolean}
    

    相关文章

      网友评论

        本文标题:如何在项目中用好TypeScript

        本文链接:https://www.haomeiwen.com/subject/udxsdrtx.html