「查漏补缺」进阶的TypeScript

966 阅读17分钟

前言

写过一篇十分钟学会typescript【语法文档】 ,包含了简单的用法,只能入个门,这里介绍一些原来没有写到的内容

我在这篇博客要介绍的内容简介如下:

  • tsconfig,能学会看懂一些基本的配置项
  • 元组的使用,跟数组的区别
  • interface接口的常用操作、继承等
  • 在类中使用typescript的重要属性、构造函数的简写、抽象类等
  • 类型保护的一些写法
  • Enum枚举类型的使用
  • 泛型的概念、泛型约束
  • 泛型在函数与class中的运用方式
  • 推荐在React中使用typescript的备忘录

tsconfig配置

先介绍一下tsconfig配置,因为非常有必要,如果没兴趣就可以跳过不看,不过建议看看。

tsconfig配置是通过tsc --init打出来的ts配置文件,如果使用框架脚手架的话就会自带,一般来说80%的人不会配的比脚手架的人更好,但是如果有独特需求,也可以知道去哪里更改。

我们看到的tsconfig配置文件长这样

//tsconfig.json
{
  "include":["demo.ts"],
  "compilerOptions": {
      //any something
      //........
  }
}

include 、exclude 和 files

这几个都是选项都是数组,include是嵌套、包含的意思,意思是要编译包含哪些文件,exclude刚好相反,就是我不要编译哪些文件,filesinclude差不多

compilerOptions

字面意思,编译选项,这是一个对象类型,这里可以配置跟编译相关的信息,比如说你编译后不想要带注释啊,或者你不希望TS话太多也可以在这里修改选项配置。

下面介绍一些重点选项

  • removeComments:编译后不会带你的注释,所以你敞开了写,不会增加文件体积
  • noImplicitAny:在你写ts代码时,如果类型推断为any类型,会报错,设置成false就不会报了,设成false可以解决TS话多问题,不过如果你的代码中有很多any类型,就建议设置成false。
function a(b) {
  return b;
} //参数 "b" 隐式具有 "any" 类型,但可以从用法中推断出更好的类型。
  • strict:就是严格模式,设置为true,就代表我们的编译和书写规范,要按照TypeScript最严格的规范来写
  • strictNullChecks 字面意思,就是严格检查null类型,设置成false就不会检查null
const a: string = null;//设置成null之后就不报错
  • rootDir 和 outDir:rootDir是告诉编译器你要编译的TS文件在哪个rootDirectory里面,outDir是告诉编译器你要把编译好的JS文件放到哪个目录下
  • sourceMap:相当于TS文件跟编译后的JS文件的一个映射,它会生成一个Map文件来记录TS文件对应JS文件的信息,如果报错了,它会告诉你在TS文件中哪一行报错了,这个要设置成true。了解就好,一般脚手架都设置好的
  • noUnusedLocals:没使用变量的时候会自动提示

小结

上面的配置选项介绍了我们开发时候有可能用的到的选项,查看tsconfig,你会发现选项真的非常多,这里我就不一一介绍了,可以通过这个网址查看具体的选项 www.tslang.cn/docs/handbo…

元组

在TS中没有元组只有数组,那么元组在TS中是什么样的东西呢?我们可以通过代码来区分。

首先定义一个数组以及类型

const array: (number | string)[] = ["a", "b", 3];

上面的类型只能模糊地告诉TS我要的数组成员是number或者string类型的,那么如果说我要强制数组中的顺序,上面的写法就不行了。TS为我们提供了元组的方式来强制定义。

const array: [string, string, number] = ["a", "b", 3];

这时候如果说改变了数组的成员顺序,那么就会报错。 报错了

元组的使用

元组能够帮助我们用于多类型数组定义限制。

大部分时候,我们是在二元数组的情况下用到元组。比如下面有个数据结构[['qiuyanxi',18,'man'],['lihuanying',36,'women']]

这就可以用到元组来强约束

const array: [string, number, string][] = [
  ["qiuyanxi", 18, "man"],
  ["lihuanying", 36, "women"],
];

要搞清楚元组和数组的区别,在理解后能在项目中适当的时候使用不同的类型。

interface接口

interface接口跟type别名非常相似,我个人喜欢type梭哈,这里介绍一下interface接口的使用

readonly 和可选属性

interface data {
  readonly id: number;//只读
  name: string;
  age?: number; //可选
}

function getName(data: data) {
  return data.name;
}
getName({ id: 1, name: "qiuyanxi", age: 18 });

function getName(data: data) {
  return data.name;
}
getName({ id: 1, name: "qiuyanxi", age: 18 });

上面的接口要求我们传入对应的属性。?表示可不加,readonly要求我们加了之后不能改。

任意属性

如果我们除了上面的类型,还需要别的,但是我不清楚要哪些怎么办?接口允许我们传任意属性。

interface data {
  readonly id: number; 
  name: string;
  age?: number; 
  [key: string]: any;//任意属性
}

function getName(data: data) {
  return data.name;
}
getName({ id: 1, name: "qiu", age: 18, x: 123 });

确定了任意属性后,那么确定的属性和可选的属性都必须被任意属性所包含,否则报错。

接口方法

interface data {
  sayHi(): void;
}

接口和类的匹配implements

在跟类的结合时,不能这样写,

class Person : data {}//报错

这样不能代表Person类所需要的类型是data接口中的定义类型

接口和类之间需要用到一个关键字:implements

interface data {
  readonly id: number;
}
class Person implements data {
  id: 1;
} //正确写法

接口之间的继承 extends

如果我有两个接口,都是相同的定义,但是第二个接口要比第一个接口多一个属性,那就可以用到接口的继承

interface data {
  id: number;
}

interface data2 extends data {
  name: string;
}
const person: data2 = {
  name: "q",
  id: 12,
};

Class中使用typescript

class Person {
  name: string;
  age: number;
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

上面的代码是TS在类中的简单使用,不过这样写太麻烦了,所以TS为我们准备了语法糖

class Person {
  constructor(public name: string, public age: number) {}
}
const a = new Person("qiu", 18);
console.log(a.name);
console.log(a.age);

类的继承 extends

TS跟ES6的类继承是一样的,同样使用extends属性,并且会把类型也继承过去

class Person {
  constructor(public name: string, public age: number) {}
}
class Teacher extends Person {}
const a = new Teacher("qiu", "18");//报错了,第二个参数要传number类型

类的重写

TS中可以重写父类的方法,

class Person {
  getA(a: number): number {
    return a;
  }
}
class Teacher extends Person {
  getA(a: number): number {
    return 123;
  }
}

不过不能重写父类的类型定义,会报错。

可以看出实际上TS就是给类上强制加了约束。

类的访问类型public、private、protected

ES6中,有类的静态方法属性、私有字段、公共属性等,那么在TS中,只要记住三个关键访问类型即可,分别是publicprivateprotected

TS中默认所有成员都是public的,也就是在类中或者类外都可以使用

class Person {
  name= "qiuyanxi";//相当于public name= "qiuyanxi"
  sayHi() {//相当于public sayHi(){...}
    console.log("hi" + this.name);
  }
}
const p = new Person();
console.log(p.name);
console.log(p.sayHi());

private属性定义只能在类中使用,相当于es6中的私有属性。

class Person {
  private name="qiuyanxi";
  sayHi() {
    console.log("hi");
  }
}
const p = new Person();
console.log(p.name); //报错
console.log(p.sayHi());

而且就算继承了,也不会继承私有属性

class Person {
  private name= "qiuyanxi";
  sayHi() {
    console.log("hi");
  }
}
class Teacher extends Person {
  sayHi() {
    console.log(this.name); //属性“name”为私有属性,只能在类“Person”中访问。
  }
}

如果说我还是想要在继承后顺便把私有属性继承过来呢?就可以使用protected关键字

class Person {
  protected name = "qiuyanxi";
  sayHi() {
    console.log(this.name);
  }
}
class Teacher extends Person {}
const p = new Teacher();
p.sayHi();

构造函数constructor

构造函数的基本使用就跟ES6是一样的,只是一般我们都习惯用TS语法糖,减少代码

//改造前
class Person {
  name: string;
  age: string;
  height: string;
  constructor(name: string, age: string, height: string) {
    this.name = name;
    this.age = age;
    this.height = height;
  }
}
//语法糖
class Person {
  constructor(public name: string, public age: string, public height: string) {}
}

继承后的子类如果有单独的属性要写,必须要写super,不然会报错。这跟es6的规则也是一样的,用TS的话在书写的时候就会提示报错。

在使用继承后,如果子类也有constructor构造函数,那么必须写super,否则报错,这也是强规定。

class Person {
  constructor(public name: string, public age: string, public height: string) {}
}

class Teacher extends Person {
  constructor(name, age, height) {
   //不写super就报错
    super(name, age, height);
  }
}

static属性

这个跟ES6也是一样的,就是类的静态属性或者方法,只能类自己外部调用罢了

可以看我另一篇文章,这里就不赘述了

juejin.cn/post/691090…

类的getter和setter

类中的gettersetter跟普通函数中的写法相差不大

class Person {
  private _name = "qiuyanxi";
  get name() {
    return this._name;
  }
  set name(value) {
    this._name = value;
  }
}
const p = new Person();
console.log(p.name); //qiuyanxi
p.name = "qiuqiuqiu";
console.log(p.name); //qiuqiuqiu

类的只读属性readonly

class Person {
  constructor(public readonly name: string) {}
}
const p = new Person("qiuyanxi");
p.name = "qiuqiuqiu";//无法分配到 "name" ,因为它是只读属性。ts(2540)

在类中使用只读属性,如果不小心修改到,TS就会报错,帮助我们管住自己手~

抽象类

抽象类实际上只是一个概念,比如说我有四个类,都有相同的方法名,但是呢每个类的方法实现的都不一样,每个类都会执行不同的逻辑,只是方法名是相同的,那么就可以使用抽象类。

抽象类的关键字就是abstract,可以理解为抽取相同的部分。

下面是中国人、日本人、美国人分别继承了sayHi的方法,但是每个人的语言都是不一样的。

abstract class Person {
  abstract sayHi();
}
class Chinese extends Person {
  sayHi() {
    console.log("你好啊");
  }
}
class Japanese extends Person {
  sayHi() {
    console.log("こんにちは");
  }
}
class American extends Person {
  sayHi() {
    console.log("hello");
  }
}

继承了抽象类后,必须重写抽象类中的方法,否则就会报错。

小结

上面所有介绍到的class用法实际上ES6都有,只是这里加上TS重新说了一遍,但是如果你写过的话就会知道,TS的语法糖和强类型有多好用了。我感觉写了TS后ES6就不太会写了。

联合类型和类型保护

联合类型就不多说了,入门的都清楚,这里说一下类型保护,比如下面的代码

function fn(a: string | number, b: string | number) {
  return a + b;//运算符“+”不能应用于类型“string | number”和“string | number”
}

在联合类型中,经常会出现这样的错误,所以我们需要用到类型保护,它是这样解决的

typeof类型保护

function fn(a: string | number, b: string | number) {
  if (typeof a === "number" && typeof b === "number") {
    return a + b;
  } else {
    return `${a}+${b}`;
  }
}

可以看到类型保护实际上就是做一个条件判断。

使用in语法类型保护

interface women {
  say(): number;
}
interface man {
  sayHi(): number;
}
function fn(arg: women | man) {
  if ("say" in arg) {
    arg.say();
  } else {
    arg.sayHi();
  }
}

instanceof类型保护

同理还有instanceof语法,经常用于Object

class Person {
  count: number = 0;
}
function addObj(first: object | Person, second: object | Person) {
  return first.count + second.count;
}// 类型“object”上不存在属性“count”。ts(2339)

上面就会报错,我们可以使用instanceof做一个判断

class Person {
  count: number = 0;
}
function addObj(first: object | Person, second: object | Person) {
  if (first instanceof Person && second instanceof Person) {
    return first.count + second.count;
  }
  return null;
}

断言保护

class Man {
  money: number = 0;
  say() {}
}
class Woman {
  money: number = 10086;
  sayHi() {}
}
function fn(a: Man | Woman) {
  if (a.money === 0) {
    (a as Man).say();
  } else {
    (a as Woman).sayHi();
  }
}

上面的代码使用了class做类型,然后通过条件判断+类型断言来进行类型保护,这种方法需要拥有一个相同的变量才可以,否则就会报错。

Enum 枚举类型

什么是枚举类型呢?看下面的代码

比如现在我有一个要求,我需要摇骰子做一个抉择。

function fn(state:number) {
  switch (state) {
    case 0:
      return '我要去喝水';
    case 1:
      return '我要去吃饭';
    case 2:
      return '我要去睡觉';
    default:
      break;
  }
}

console.log(fn(1));//我要去吃饭

使用枚举类型就可以这样写

enum State {
  drink,
  rice,
  sleep,
}
function fn(state:number) {
  switch (state) {
    case State.drink:
      return '我要去喝水';
    case State.rice:
      return '我要去吃饭';
    case State.sleep:
      return '我要去睡觉';
    default:
      break;
  }
}
console.log(fn(1));//我要去吃饭

使用枚举类型帮助我们更好地语义化了这段代码,枚举类型自带下标,从0开始逐一递增,我们当然可以自行设置下标。比如下面我们设置成string

enum State {
  drink = "123",
  rice = "456",
  sleep = "789",
}

很多人都不太理解这是啥玩意,我们编译出来就知道这是用来干嘛的了

//编译后
var State;
(function (State) {
    State["drink"] = "123";
    State["rice"] = "456";
    State["sleep"] = "789";
})(State || (State = {}));

可以看出枚举类型编译后变成了一个立即执行函数外加一个全局变量,实际上它就类似一个对象,帮我们存值,很多时候我们为了优化代码,会使用表结构编程,即运用一个对象来包裹处理逻辑,像下面这段代码

//优化前
function fn(a) {
  if (a === 1) {
    console.log('我要去喝水');
  } else if (a === 2) {
    console.log('我要去吃饭');
  } else if (a === 3) {
    console.log('我要去睡觉');
  } else {
    console.log("没了");
  }
}
//优化后
const hashMap = {
  摇骰子为1: () => {
    console.log("我要去喝水");
  },
  摇骰子为2: () => {
    console.log("我要去吃饭");
  },
  摇骰子为3: () => {
    console.log("我要去睡觉");
  },
  没有的情况: () => {
    console.log("没了");
  },
};
const obj = { 1: "摇骰子为1", 2: "摇骰子为2", 3: "摇骰子为3" };
function fn(a) {
  if (obj[a]) {
    hashMap[obj[a]]();
  } else {
    hashMap["没有的情况"]();
  }
}
fn(1);
//再使用enum优化一下
enum hashMap {
  throwOne = 1,
  throwTwo = 2,
  throwThree = 3,
}
const obj = {
  throwOne: () => {console.log("吃饭")},
  throwTwo: () => {console.log("睡觉")},
  throwThree: () => {console.log("打豆豆")},
  没有的情况: () => {console.log("没了")}};
function fn(a: number) {
  if (hashMap[a]) {
    obj[hashMap[a]]();
  } else {
    obj["没有的情况"]();
  }
}
fn(1);

用了enum之后语义化增强了一些,虽然感觉表结构代码有点多了,不过处理的逻辑代码少了很多。

使用enum可以反向查值,例如上面的代码我用了hashMap[a]来反向查值

enum的作用

实际工作中统一处理业务逻辑、类型定义,有很多时候都能用的到enum,就比如redux的类型定义,不过我相信你们一定能找到更好的方式。

enum actionType {
  ADD = "add",
  UPDATE = "update",
  DELETE = "delete",
}
const { ADD } = actionType;
//使用时
dispatch(ADD);

不过用这个确实很装逼啊!

泛型

泛型应该是TypeScript中最难的概念了,泛型一般用于函数、类中,我对泛型的看法就是:它提供一个通用的约束,在这个约束内,你可以随便写。

举个例子,下面是一个函数,我要求它传入两个参数,可以是string也可以是number,但是如果前面传了string,后面就需要相同的类型。我们使用联合类型写这么一段代码

function fn(a:number|string,b:number|string){
//现在我已经限制了参数,但是如何才能限制参数要保持一致呢?

}

TypeScript给出的方法是,我给你一个泛用的类型,你定义函数的时候使用泛型,自己调用的时候可以约束类型。上面的代码使用泛型就可以改成这样

function fn<T>(a: T, b: T) {}
//调用的时候自己约束
fn<string>("1", "2");
fn<string>(1, "2");.//会报错

泛型就是使用<T>的时候来定义的,一般情况下我们会用T字母表示泛型。

多个泛型

我现在又改需求了,我想要让第一个参数用string,第二个参数用number,怎么做?可以采用多个泛型。

function fn<T, P>(a: T, b: P) {}
//调用的时候自己约束
fn<string, number>("1", 2);

泛型的约束

上面的例子中有讲到,我一定要让函数的参数在numberstring之间,并且参数要保持一致的类型,泛型只实现了后半截,前半截如何实现?这就需要用到泛型的约束,泛型约束采用extends关键字,没错就是继承

function fn<T extends number | string>(a: T, b: T) {}
fn<string>("123", "456");
fn<string>("123", 456); //报错了,要求一致都是 string

我们也可以配合接口来约束函数参数的属性

interface IWithLength{
  length:number
}
function echoWithLength<T extends IWithLength>(arg:T){
...
}

上面的代码就要求参数需要包含 length 属性。

interface 和泛型

interface KeyPair<T, U> {
  key: T;
  value: U;
}

let kpi: KeyPair<number, string> = {
  key: 123,
  value: "name",
};

配合interface 的使用,泛型给了一个通用的约束,约束的内容需要你自己写出来。

interface、函数参数、泛型结合

下面的函数是这样定义的

interface IPlus {
  (a: number, b: number): number;
}
function plus(a: number, b: number): number {
  return a + b;
}
const ace: IPlus = plus;

如果以后可能要支持 string 类型参数,我们也许会这样写

interface IPlus {
  (a: number, b: number): number;
}
interface IPlus2 {
  (a: string, b: string): string;
}
function plus(a: number, b: number): number {
  return a + b;
}
function concat(a:string,b:string):string{
  return a + b
}
const ace: IPlus = plus;

const ace2: IPlus2 = concat

但是这样实在太麻烦,所以我们需要使用泛型改造一下,省略掉一个声明

  interface IPlus<T> {
    (a: T, b: T): T;
  }

  function plus(a: number, b: number): number {
    return a + b;
  }
  function concat(a: string, b: string): string {
    return a + b;
  }
  const ace: IPlus<number> = plus;

  const ace2: IPlus<string> = concat;

数组的使用

我再次改需求了,我现在只想要一个参数,这个参数传数组,内部为string,我们不用泛型可以这么写

function fn(a: string[]) {}
fn(["1"]);

使用泛型可以这么写

function fn<A>(a: A[]) {}
fn(["1"]);

也可以这么写

function fn<A>(a: Array<A>) {}
fn(["1"]);

下面的代码是等价的

Array<number> ==> number[]

小结

泛型其实是一种宽泛的类型,就是允许你自己定义一个通用的约束,然后怎么写TS就不管了。由于泛型的概念跟之前我们学习的TS静态类型定义不太一样,打破了我们固有的强类型指定的印象,所以就觉得这个概念很难理解。

//这种是强类型定义
const a:string='string' 

//这种是泛型,不管你传啥,反正参数要跟返回值保持一致,至于是啥你自己定
function fn<T>(args: T): T {
  return args;
}

泛型在class中的使用

下面我们来定义一个class,这个class接收一个并不是很明确的数组

class Person {
  constructor(
    public message:
      | {
          name: string;
          id: number;
        }[]
      | {
          name: string;
          id: number;
          age: number;
        }[]
  ) {}
}

上面的class可以接收一个并没有严格明确内容的数组,我这个数组中的成员可以要age,也可以不要。那这样写就很呆,我们可以用泛型来简化一下

class Person<T> {
  constructor(public message: Array<T>) {}
}

调用的时候自己写数据规则

//自己写数据规则
interface Info {
  name: string;
  id: number;
}
interface Information extends Info {
  age: number;
}
class Person<T> {
  constructor(public message: Array<T>) {}
}
//自己用数据规则
const a = new Person<Info>([ //注意看这里调用时也用到了泛型
  { name: "qiu", id: 1 },
  { name: "yan", id: 2 },
  { name: "xi", id: 3 },
]);

在类中给泛型做一个约束

现在我要给类写一个方法,这个方法是获取到某个姓名 会发现这时候是报错的,因为我们并没有进行约束,也就是没有告诉泛型,它里面有啥内容。所以我们需要用到extends关键字

interface Info {
  name: string;
  id: number;
}
class Person<T extends Info> {//这里继承一下
  constructor(public message: Array<T>) {}
  getName(id: number) {
    return this.message.filter((m) => m.id === id)[0].name;
  }
}

然后就没问题了~已经做好了约束,现在已经给泛型做了保底,哪怕传入的数据有更多的类型,也没问题。

interface Information extends Info {
  age: number;
}
const a = new Person<Information>([
  { name: "qiu", id: 1, age: 2 }, 
  { name: "yan", id: 2, age: 3 },
  { name: "xi", id: 3, age: 4 },
]);

小结

在类中使用泛型可以简化我们程序的可读性,通过继承接口的形式可以约定泛型的类型或者类型,做一个保底。

在React中使用Typescript

React中,我们写相关代码都已经有了官方声明的,比如使用组件时采用FCprops采用泛型啦,都已经被规定好了,相信大家总是健忘不知道要给啥类型,这里提供一个很好的备忘录给大家参考

英文版备忘录: github.com/typescript-…

有掘金的大神翻译好的备忘录 juejin.cn/post/691086…

还有一份TS类型别名参考 juejin.cn/post/684490…