TypeScript 总结篇

1,728 阅读14分钟

前言

本文是个人学习TypeScript中的个人总结, 其中主要是一些重要常用的点, 还有一些不太好理解的问题, 同时也参考了一些网上文章, 希望可以帮助初学者快速入门,如有不对之处, 还望指出,感谢~

类型

基础类型

  • 数字(number):包括浮点数,以及 NaN、±Infinity。
  • 字符串(string):字符串型。
  • 布尔(boolean):布尔型,即 { true, false }。
  • null:即 { null }。
  • undefined:即 { undefined }。 默认情况下null和undefined是所有类型的子类型,可以赋值给任何类型;当你指定了--strictNullChecks标记,null和undefined只能赋值给void和它们各自
  • symbol:符号类型。
  • never:表示永远无法到达的终点,是任何类型的子类型,也可以赋值给任何类型,但除自身之外任何类型都不能赋值给它。一个中途抛出错误,或者存在死循环的函数永远不会有返回值,其返回类型是 never
  • void:表示没有任何类型,没有返回值;它等于 { null, undefined }。
  • 元组(tuple):特殊的数组,指定数组每项具体的类型,一一映射。eg: let tuple1: [number, string] = [12, 'aa']
  • 枚举(enum):一组固定值的特定映射。eg: enum sex { MALE, FEMALE} 等同于 enum sex {MALE=0, FEMALE=1} sex.MALE // 取值
  • 任意类型(any)
  • unknow
  • object表示非原始类型,也就是除number,string,boolean,symbol,null或undefined之外的类型 另外,所有原始类型的字面量本身也可作为类型使用,其外延只包括自身

高级类型

  • 联合类型
    表示一个值可以是几种类型之一, 相当于集合交集. 竖线(|)分隔; 若一个值是联合类型,只能访问此联合类型的所有类型里共有的成员

  • 交叉类型
    将多个类型合并为一个类型, 相当于集合并集. 竖线(&)分隔;把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性,多用于mixins

  • 映射类型:从旧类型创建新类型的一种方式 (包含Readonly Partial Record Pick)

        type Readonly<T> = {
            readonly [P in keyof T]: T[P];
        }
        type Partial<T> = {
            [P in keyof T]?: T[P];
        }
    
        interface Person {
            name: string;
            age: number;
        }
    
        // 调用
        type PersonPartial = Partial<Person>;
        type ReadonlyPerson = Readonly<Person>;
    
        // 结果
        type PersonPartial = {
            name?: string|undefined;
            age?: number|undefined;
        }
        type PersonReadonly {
            readonly name: string;
            readonly age: number;
        }
    

类型保护

  1. 用户自定义类型保护:定义函数,返回类型谓词(语法:parameterName is Type)

  2. typeof类型保护:只有两种形式能被识别typeof v === "typename"typeof v !== "typename", "typename"必须是 "number", "string", "boolean"或 "symbol" 原始类型,若为除前四个的其他字符串,将不被识别为类型保护

  3. instanceof 类型保护:过构造函数来细化类型的一种方式。语法: 实例 instanceof 构造函数

       function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] {
         return names.map(n => o[n]);
       }
       
       interface Person {
           name: string;
           age: number;
       }
       let person: Person = {
           name: 'Jarid',
           age: 35
       };
       let strings: string[] = pluck(person, ['name']); // ok, ['Jarid]
    

    keyof T, 索引类型查询操作符 (此实例中,为person的属性name、age,值等于'name'|'age')
    K extends keyof T,泛型约束
    T[K], 索引访问操作符(此实例中,为person['name'])

小结:(以下图来自于网络, 感谢~)

高级类型脑图

类型检查机制脑图

泛型

泛型(Generics)是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。
类型变量(类型参数) 一般用T表示

泛性约束 extends

interface Lengthwise {
    length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);
    return arg;
}

泛型接口

使用含有泛型的接口来定义函数形状

    interface ConfigFn{
        <T>(value:T):T;
    }

    var getData:ConfigFn=function<T>(value:T):T{
        return value;
    }

    getData<string>('张三');
    getData<string>(1243);  //错误
  // 把类型参数提到接口名上
  // 写法一:
    interface ConfigFn<T>{
        (value:T):T;
    }

    var getData:ConfigFn<string>=function<T>(value:T):T{
        return value;
    }

    getData('20');  /*正确*/


  // 写法二:
    interface ConfigFn<T>{
        (value:T):T;
    }

    function getData<T>(value:T):T{
        return value;
    }
    
    var myGetData:ConfigFn<string>=getData;     
    myGetData('20');  /*正确*/
    myGetData(20)  //错误

泛型类

使用泛型来定义类

  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; };

抽象类、抽象方法、继承关系

  • 抽象类
1、抽象类是提供其他类继承的基类,不能直接被实例化.
2、用abstract关键字定义抽象类和抽象方法,抽象类中的抽象方法不包含具体实现并且必须在派生类中实现.
3、abstract抽象方法只能放在抽象类里面.
4、可以使用修饰符.
5、abstract 修饰,里面可以没有抽象方法。但有抽象方法(abstract method)的类必须声明为抽象类(abstract class)
  • 多态
    父类定义一个方法不去实现,让继承它的子类去实现,每一个子类有不同的表现(用于继承)

注意

1.使用多态基础是类的继承或者接口实现。
2.如果子类继承的是一个抽象类,子类必须实现父类里的抽象方法,不然的话不能实例化,会报错。
3.多态的子类继承父类,子类可以不实现该方法,但使用多态就是子类为了实现方法

typeof Class 构造函数

class Greeter {
static standardGreeting = "Hello, there"; // 静态属性
greeting: string;
greet() {
    if (this.greeting) {
        return "Hello, " + this.greeting;
    } else {
        return Greeter.standardGreeting;
    }
}
}

// typeof Greeter: 构造函数的类型, 包含类的静态成员和构造函数
let greeterMaker: typeof Greeter = Greeter;
greeterMaker.standardGreeting = "Hey there!";

// Greeter: 实例类型
let greeter2: Greeter = new greeterMaker();
console.log(greeter2.greet());

接口

在面向对象的编程中,接口是一种规范的定义,它定义了行为和动作的规范; 接口定义了某一批类所需要遵守的规范,不关心这些类的内部状态数据和其中方法的实现细节,它只规定这批类里必须提供某些方法,提供这些方法的类就可以满足实际需要.

  • 面向对象语言:接口是对行为和动作的抽象,而具体如何行动需要由类(classes)去实现(implement)
  • ts: 对类的部分行为进行抽象,对对象的形状进行描述; 把不同类中的共性抽取作为接口,再由类分别去实现(implement),相对于面向对象语言, 还增加了更灵活的接口类型,包括属性、函数、可索引和类等.
    简而言之,定义规范标准,用于复用(与普通类型约束相比)。
interface Person {
    readonly id: number; // 只读属性
    name: string; // 确定属性
    age?: number; // 可选属性
    [propName: string]: string; // 字符串索引
    [index: number]: string; // 数字索引,类似数组
    (name: string, age: number): void; // 函数
    getName(id: number): string; // 方法
    new(name: string, age: number): Person; // 构造函数
}

:
1.上面只是展示接口支持的功能, 不代表可以全部写在一起.
2.若数字索引和字符串索引同时存在, 数字索引的返回值必须是字符串索引返回值类型的子类型. 因为当使用 number来索引时,JavaScript会将它转换成string然后再去索引对象
3.字符串索引与其他属性同时存在, 其他属性值类型必须是字符串索引返回值类型的子类型. 因为字符串索引声明了 obj.property和obj[“property”]两种形式都可以

类类型

类实现接口, 用接口来明确的强制一个类去符合某种契约

// 接口描述类的实例部分
interface ClockInterface {
    currentTime: Date;
    setTime(d: Date);
}

class Clock implements ClockInterface {
    currentTime: Date;
    setTime(d: Date) {
        this.currentTime = d;
    }
    constructor(h: number, m: number) { } // 类静态部分
}

类静态部分与实例部分的区别

类分为静态和实例部分:

  • 静态部分
    类的构造函数和所有静态成员
  • 实例部分
    类的实例属性和实例方法, 在构造函数的原型链上

当你用构造器签名去定义一个接口并试图定义一个类去实现这个接口时会得到一个错误, 因为当一个类实现了一个接口时,只对其实例部分进行类型检查。 constructor存在于类的静态部分,所以不在检查的范围内
在实现接口时, 若想对构造函数和实例部分做约束, 可以分别约束, 并通过一个新的构造函数(如下:createClock)生成实例

//  ClockConstructor 为构造函数所用
//  ClockInterface 为实例方法所用
//  createClock的第一个参数是ClockConstructor类型,在createClock(AnalogClock, 7, 32)里,会检查AnalogClock是否符合构造函数签名。
interface ClockConstructor {
    new (hour: number, minute: number): ClockInterface;
}
interface ClockInterface {
    tick();
}

// 第一个参数ctor的类型是接口 ClockConstructor,在这里就为类的静态部分指定需要实现的接口
function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {
    return new ctor(hour, minute);
}

// 类 DigitalClock 实例化出来的对象(类的实例部分)应该满足这个接口的规则
class DigitalClock implements ClockInterface {
    constructor(h: number, m: number) { }
    tick() {
        console.log("beep beep");
    }
}
class AnalogClock implements ClockInterface {
    constructor(h: number, m: number) { }
    tick() {
        console.log("tick tock");
    }
}

let digital = createClock(DigitalClock, 12, 17);
let analog = createClock(AnalogClock, 7, 32);

混合类型

// 函数接口
interface Counter {
  (start: number): string
}

let counter: Counter
counter = function (start: number) {
  console.log(number)
}
// 调用
counter(12)
// 对象接口
interface Counter {
    interval: number;
    reset(): void;
}

下面的官方例子, 混合类型等同于上面两个接口声明合并

// 一个对象可以同时做为函数和对象使用,并带有额外的属性。如:下文中的变量c
interface Counter {
    (start: number): string; // 函数
    interval: number; // 对象属性
    reset(): void; // 对象方法
}

function getCounter(): Counter {
    // 通过类型断言,将函数对象转换为Counter类型,转换后的对象不但实现了函数接口的描述,使之成为一个函数,还具有interval属性和reset()方法
    let counter = <Counter>function (start: number) { console.log(number) }; 
    counter.interval = 123;
    counter.reset = function () { };
    return counter;
}

let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;

混合类型主要运用于类似UMD库, 如老朋友jQuery, 既可以作为函数,也可以作为对象使用, 最近看Vue3的源码, 发现里面也存在, 如下:

export interface ReactiveEffect<T = any> {
  (): T
  _isEffect: true
  active: boolean
  raw: () => T
  deps: Array<Dep>
  options: ReactiveEffectOptions
}

function createReactiveEffect<T = any>(
  fn: () => T,
  options: ReactiveEffectOptions
): ReactiveEffect<T> {
  const effect = function reactiveEffect(...args: unknown[]): unknown {
    return run(effect, fn, args)
  } as ReactiveEffect
  effect._isEffect = true
  effect.active = true
  effect.raw = fn
  effect.deps = []
  effect.options = options
  return effect
}

类与接口

  • 类继承类(class extends class)
  • 类实现接口(class implements interface)
  • 接口继承接口(interface extends interface)
  • 接口继承类(interface extends class)

ps: 此处主要讲下,接口继承类, 其他大家应该理解, 或可自行查阅文档

接口继承类

接口继承类时,会继承类的所有成员但不包含实现

  class Person {
      type: string // ️这里是类的描述
  }

  interface Child extends Person { // ️Child 接口继承自 Person 类,因此规范了 type 属性
      log(): void
      // 这里其实有一个 type: string
  }

  // 上面的 Child 接口继承了 Person 对 type 的描述,还定义了 Child 接口本身 log 的描述

  // 第一种写法
  class Girl implements Child {
      type: 'child' // 接口继承自 Person 的
      log() {} // 接口本身规范的
  }

  // 第二种写法
  class Boy extends Person implements Child { // 首先 extends 了 Person 类,然后还需满足 Child 接口的描述
      type: 'child'
      log() {}
  }

当接口继承了一个拥有私有(private)或受保护(protected)的成员的类时,这个接口类型只能被这个类或其子类所实现(implement)。

  class Person {
      private type: string // ️这里是类的描述
  }

  interface Child extends Person { // ️Child 接口继承自 Person 类,因此规范了 type 属性
      log(): void
      // 这里其实有一个 type: string
  }

  //  上面的 Child 接口继承了 Person 对 type 的描述,还定义了 Child 接口本身 log 的描述


  // 写法 (only)
  class Boy extends Person implements Child { // 首先 extends 了 Person 类,然后还需满足 Child 接口的描述
      type: 'child'
      log() {}
  }

总的来说,这种模式是应当避免的(不推荐),尤其是在类拥有私有成员时.

函数

定义函数

1、函数声明

function sum(x: number, y: number): number {
    return x + y;
}

2、函数表达式

  let mySum = function (x: number, y: number): number {
      return x + y;
  };

  // 或者
  let mySum: (x: number, y: number) => number = function (x: number, y: number): number {
      return x + y;
  };

注意:
1.可选参数(?:): 必须接在必需参数后面
2.参数默认值: 相当于可选参数,任意位置
3.剩余参数(...rest): 必须是在最后,与 es6 相同

重载

java中的重载:同名函数,参数不一样. 允许一个函数接受不同数量或类型的参数时,作出不同的处理. typescript中的重载:通过为同一个函数提供多个函数类型定义,一个函数体实现多种功能的目的. ts为了兼容es5 以及 es6 重载的写法和java中有区别.

function reverse(x: number): number;  // 函数定义
function reverse(x: string): string;  // 函数定义
function reverse(x: number | string): number | string {  // 函数实现
  if (typeof x === 'number') {
      return Number(x.toString().split('').reverse().join(''));
  } else if (typeof x === 'string') {
      return x.split('').reverse().join('');
  }
}

注意:

 1.返回结果不同时需要使用重载, 仅是参数不同,可以使用联合类型  
 2.此外, 重载的顺序应该是匹配范围从小到大, 因为重载匹配是从上到下, 匹配后不再往下查找

配置文件tsconfig.json

指定编译选项和项目待编译文件, 主要包含以下选项:

  • compilerOptions 用来配置编译选项
  • files 用来指定待编译文件
  • include: 指定待编译文件
  • exclude: 排除待编译文件

TS 文件指拓展名为 .ts、.tsx 或 .d.ts 的文件。如果开启了 compilerOptions.allowJs 选项,那 .js 和 .jsx 文件也属于 TS 文件

与待编译文件相关的选项

1. 若files 和 include 都未设置,那么除了 exclude 排除的文件,编译器会默认包含路径下的所有 TS 文件。
2. 若同时设置 files 和 include ,那么编译器会把两者指定的文件都引入
3. 若未设置 exclude ,那其默认值为 node_modules 、bower_components、jspm_packages 和编译选项 outDir 指定的路径
4. exclude 只对 include 有效,对 files 无效
5. {
   "compilerOptions": {
       "typeRoots" : ["./typings"], // typeRoots指定的目录下的包, 会被编译, 即指定被编译包所在目录
       "types": ["node", "lodash", "express"] // 指定被编译的包
   }
}

compilerOptions中的选项

--target: 指定编译后的js版本
--lib: 编译中包含的库
// 其他参考官方文档

需要注意的点:

1.不带任何输入文件的情况下调用tsc,编译器会从当前目录开始去查找tsconfig.json文件,逐级向上搜索父目录。  
2.不带任何输入文件的情况下调用tsc,且使用命令行参数--project(或-p)指定一个包含tsconfig.json文件的目录。  
3.当命令行上指定了输入文件时,tsconfig.json文件会被忽略, 按照默认选项编译该文件

其他易混淆点

interface 与 type 声明类型的区别

定义类型有两种方式: 接口(interface) 和类型别名(type alias)

1、interface 只能定义对象类型或者函数, type 还可以定义组合类型,交叉类型(&,类似并集),联合类型(|,类似交集),原始类型
2、interface 方式可以实现接口的 extends 和 implements , 而 type alias 则不行。
3、interface 可以实现接口的合并,但 type alias 则不行。

unknown 与 any

TypeScript 3.0 引入了新的unknown 类型,它是 any 类型对应的安全类型。

unknown: 在对 unknown 类型的值执行大多数操作之前,必须进行某种形式的检查。
any: 在对 any 类型的值执行操作之前,不必进行任何检查。

关于unknown可参考 juejin.cn/post/684490…

const 与 readonly

两者定义或修饰的值, 不能更改, 只读

const: 用来定义(变)常量
readonly: 用来修饰对象属性

最后

我觉得学习一门语言,在熟悉文档概念后, 最终应该在实践加深体会,多踩踩坑,才能更好的掌握,毕竟学习的目的就是为了更好的为工作业务服务, 现在TS已经比较成熟了,大家肯定都比较熟,若还未上车,可以开始了.
之后有时间在总结下自己开发中遇到的问题, 感谢~~~ 关于文中不好不妥之处,还请多多包含并指出,第一次在掘金写,手动捂脸!

参考

www.typescriptlang.org/ www.tslang.cn/ ts.xcatliu.com/advanced/cl…
www.softwhy.com/article-860…
blog.poetries.top/2019/09/03/…
kbscript.com/2017/01/27/…
juejin.cn/post/684490…