Typescript 进阶篇一

499 阅读12分钟

最近使用 vue3+typescript 开发的新项目,让我对 TypeScript 有个一个更新的认识。TypeScript 发展至今,已经成为大型项目的标配,其提供的静态类型系统,大大增强了代码的可读性以及可维护性: 能够更早的发现代码中的错误、支持 JavaScript 语言的最新特性以及相同的语法和语义、跨平台。本系列博客会将我对 TypeScript 学习做一个回顾和总结。

类型别名

类型别名用来给一个类型起个新名字;它只是原类型的一个引用;可以简化程序,提高可读性和可维护性;语法 type AliasName = Type

interface IPerson {
  id: string;
  age: number;
}
interface IWorker {
  companyId: string;
}
type exprA = IPerson & IWorker;
let objA:exprA = {id: "1",age:24,companyId: "1"}  //编译成功

// 类型别名引用的类型为接口类型时可递归引用自身
type rolesT= {
  id: number;
  menuCode: string;
  children : rolesT[]};

let roles:rolesT = {
    id:1,
    menuCode : "001",
    children : [
    {
        id:1,
        menuCode : "001",
        children : []
    }

    ]
}

定义了一件事物的抽象特点,包含它的属性和方法

类的用法

存取器

存储器是实现数据封装的一种方式,它提供了一层额外的访问控制。类可以将成员变量的访问权限制在内部,在类外部通过存储器方法间接访问成员变量,在存储器方法中,还可以加入额外的访问控制等处理逻辑

  • 如果一个类属性同时定义了 get 和 set 方法,那么 get 方法的返回值类型必须与 set 方法的参数类型一致
  • 如果一个类属性同时定义了 get 和 set 方法,那么这两种方法必须具有相同的可访问性
//成员存储器
class Circle {
  private _radius: number = 4;
  get radius(): number {
    console.log("get---" + this._radius);
    return this._radius;
  }
  set radius(vaule: number) {
    console.log("set---" + vaule);
    if (vaule >= 4) {  //访问控制逻辑
      this._radius = vaule;
    }
  }
}
const circle = new Circle();
circle.radius = 5;
console.log(circle.radius);

实例成员和静态成员

  • 实例成员包括实例属性和实例方法,通过 this 调用,可以被实例继承
  • 静态成员包括静态属性和静态方法,通过类名调用,不可以被实例继承
class Circle {
  static str = "Hello World"; //静态属性
  _radius: number = 4; //实例属性
}
class Shape extends Circle {
  type = "circle";
}
const mkCircle = new Circle();
const mkShape = new Shape();
mkCircle._radius = 8;
Circle.str = "Hello Circle";
console.log(mkCircle._radius); // 8  实例属性被实例继承
console.log(mkShape._radius); // 4

console.log(Circle.str); //Hello Circle  通过类名调用静态属性
console.log(Shape.str); //Hello Circle

成员可访问性

  • public 修饰的属性或方法是公有的,没有访问限制,可在类的内外部、子类内外部访问
  • protected 修饰的属性或方法是受保护的,只允许在当前类的内部和其子类的内部访问,不允许在当前类外部访问
  • private 修饰的属性或方法是私有的,只允许在当前类的内部访问
  • 子类允许放宽父类成员的可访问性(即如果父类中成员为受保护类型,子类重写后可标记成 public),这种情况下,以子类重写的类型可访问性为主
class Circle {
  type = "circle"; //公有属性
  protected _radius: number = 4; //受保护属性
  private des = "Hello Circle"; //私有属性
  constructor() {
    console.log("类内部访问公有成员---" + this.type);
    console.log("类内部访问受保护成员---" + this._radius);
    console.log("类内部访问私有成员---" + this.des);
  }
  changeRadius() {
    this._radius = 8;
  }
}
class Shape extends Circle {
  constructor() {
    super();
    this.type = "shape"; //子类内部访问公有成员
    console.log("子类内部访问公有成员---" + this.type);
    console.log("子类内部访问受保护成员---" + this._radius);
    //console.log("子类内部访问私有成员---" + this.des); //编译错误,私有属性只能在当前类内部访问
  }
}
const mkCircle = new Circle();
const mkShape = new Shape();
console.log("类外部访问公有成员---" + mkCircle.type);
console.log("子类外部访问公有成员---" + mkShape.type);
console.log("类外部访问受保护成员---" + mkCircle._radius); //编译错误,受保护属性不能在当前类或其子类外部访问
console.log("子类类外部访问受保护成员---" + mkShape._radius); //编译错误,受保护属性不能在当前类或其子类外部访问
console.log("类外部访问私有成员---" + mkCircle.des); //编译错误,私有属性只能在当前类内部访问
console.log("子类类外部访问私有成员---" + mkShape.des); //编译错误,私有属性只能在当前类内部访问

//子类允许放宽父类成员的可访问性
class ShapeA extends Circle {
  public _radius: number = 4; //重写父类(基类)成员,类型标记成 public,此时该属性变为公有属性,可以在任何地方被访问到
  constructor() {
    super();
    console.log("内部访问---" + this._radius);
  }
}
const mkShapeA = new ShapeA();
console.log("外部访问---" + mkShapeA._radius);

类的继承

extends 继承

  • 使用 extends 关键字来指定要继承的类,语法 : class DerivedClass extends BaseClass
  • 当子类(派生类)继承了父类(基类),就自动继承了父类的非私有成员
class Dialog {
  title: string;
  visible: boolean = false;
  constructor(title: string) {
    this.title = title;
  }
  open() {
    console.log("Dialog --- open");
    this.visible = true;
  }
}

class Drawer extends Dialog {
  position: string = "left";
  toSetPosition(position) {
    this.position = position;
    console.log(`Drawer---------${this.position}`);
  }
}
let mkDrawer = new Drawer("新增");
mkDrawer.open(); //Dialog --- open
mkDrawer.toSetPosition("right"); //Drawer---------right

重写父类(基类)成员

  • 在子类中定义与父类中同名的成员变量和成员函数,可以重写父类成员
  • 当子类和父类存在同名的非私有成员时。子类只能通过关键字 super 访问父类的非私有成员
  • 子类允许放宽父类成员的可访问性(即如果父类中成员为受保护类型,子类重写后可标记成 public)
 class Dialog {
  protected title: string;
  visible: boolean = false;
  constructor(title: string) {
    this.title = title;
  }
  open() {
    console.log("Dialog --- open");
    this.visible = true;
  }
}

class Drawer extends Dialog {
  position: string = "left";
  open() {  //父类成员函数open被重写
    console.log("Drawer --- open");
    this.visible = true;
    super.open(); //如果想调用父类的成员函数,需要使用关键词super来访问
  }
  toSetPosition(position) {
    this.position = position;
    console.log(`Drawer---------${this.position}`);
  }
}
let mkDrawer = new Drawer("新增");
mkDrawer.open(); //Drawer --- open  Dialog --- open
mkDrawer.toSetPosition("right"); //Drawer---------right

子类实例化

  • 子类如果包含了构造函数中,必须调用 super(),执行父类的构造函数来实例化子类
  • 实例化子类的初始化顺序 : 初始化父类的属性 =》 调用父类的构造函数 =》初始化子类的属性 =》调用子类的构造函数
  • 构造函数里访问 this 的属性之前,必须调用 super()实例化,因为子类未实例化前访问会编译编译报错
class Dialog {
  //初始化父类的属性
  title: string;
  visible: boolean = false;
  content: string = "Hello Dialog!";
  //调用父类的构造函数
  constructor(title: string) {
    console.log(`Dialog父类属性--${this.content}`);
    console.log(`调用父类的构造函数`);
    this.title = title;
  }
  open() {
    console.log("Dialog --- open");
    this.visible = true;
  }
}

class Drawer extends Dialog {
  //初始化子类的属性
  position: string = "left";
  content: string = "Hello Drawer!";
  //调用子类的构造函数
  constructor(title: string) {
    super(title); //
    this.position = "right";
    console.log(`Drawer子类属性--${this.content}`);
    console.log(`调用子类的构造函数`);
  }
  open() {
    console.log("Drawer --- open");
    this.visible = true;
    super.open(); //如果想调用父类的成员函数,需要使用关键词super来访问
  }
}
let mkDrawer = new Drawer("新增");
mkDrawer.open(); //Drawer --- open

单继承

ts 中的类仅支持单继承,不支持多继承,只能指定一个父类

类的类型继承

接口继承类

声明 class,除了会创建一个类之外,同时也创建了类的类型(类型只包含其中的公有实例属性和实例方法,不包括构造函数、静态属性或静态方法);接口继承类继承的实际上是类的实例的类型定义。

class Dialog {
  title: string;
  visible: boolean = false;
  constructor(title: string) {
    this.title = title;
  }
  open() {
    this.visible = true;
  }
}

//class创建的类作为类型定义使用
let dia: Dialog = {
  title: "提示",
  visible: false,
  open() {
    console.log("open");
  }
};

//class被接口继承

interface DialogExtra extends Dialog {
  content: string;
}
let diaExt: DialogExtra = {
  title: "提示",
  visible: false,
  content: "Hello",
  open() {
    console.log("open");
  }
};

类实现接口

类可以实现接口或类;实现一个新的类,从父类或者接口的定义实现所有的属性和方法

interface AnimalA {
  name: string;
  eat: () => void;
}
interface AnimalB {
  sex: string;
  bark: () => void;
}
class AnimalC {
  sex: string;
  constructor(sex: string) {
    this.sex = sex;
  }
  bark() {
    console.log("bark");
  }
}
//类实现接口
//此时AnimalA、AnimalB作为约束的类型定义,实现的新类需符合AnimalA、AnimalB的类型约束,在此基础上可增加自定义的属性和方法
class DogA implements AnimalA、, AnimalB {
  name: string = "dog";
  sex: string = "女";
  eat() {
    console.log("tag", "I love eat bone!");
  }
  bark() {
    console.log("tag", "I love bark!");
  }
  drink() {  //在AnimalA、, AnimalB类型定义的属性基础上,也可增加新的属性和方法
    console.log("tag", "I want to drink!");
  }
}
const dog = new DogA();
dog.eat();

//类实现类
//此时的AnimalC类是作为类的类型约束实现的新类DogB,在满足AnimalC类型约束的基础上可增加自定义的属性和方法
class DogB implements AnimalC {
  name: string = "dog";
  sex: string = "女";
  bark() {
    console.log("tag", "I love bark!");
  }
}
const dogB = new DogB();
dog.bark();

接口继承

接口与接口之间可以直接进行多继承

  • 一个接口可以同时继承多个接口,实现多个接口成员的合并。用逗号隔开要继承的接口。
  • 继承的接口中,定义的同名属性的类型不同的话,是不能编译通过的

interface AnimalA {
  name: string;
  eat: () => void;
}
interface AnimalB {
  sex: string;
  bark: () => void;
}
interface AnimalC {
  sex: number;

}
interface Dog extends AnimalA, AnimalB { //一个接口可以同时继承多个接口
  desription: string;
}
const dog: Dog = {
  name: "空空",
  sex: "女",
  desription: "空空很可爱",
  bark() {
    console.log("bark");
  },
  eat() {
    console.log("meat");
  },
};


interface Dog extends AnimalB,AnimalC { //编译出错 ,AnimalB、AnimalC定义的sex属性的类型不同
  desription: string;
}

泛型

泛型,即“参数化类型”,泛型允许再程序中定义形式类型参数,然后在泛型实例化时使用实际类型参数来替换形式类型参数,

形式类型参数

泛型类型参数能够表示绑定到泛型类型或泛型函数调用的某个实际类型,在类声明、接口声明、类型别名声明以及函数声明中都支持定义类型参数;语法 <TypeParameter.TypeParameter,...>

形式参数两种命名风格

  • 大写字母 T 加描述性名称,例如 TResult
  • 使用单个大写字母,由字母 T 开始,依次使用后续的 U 、V 等大写字母

类型参数默认类型 <T=DefaultType>

  • 当设置了默认类型,该类型参数是一个可选类型参数
function identity<T = string>(arg: T): T {
  return arg;
}
console.log(identity<string>("HelloWord"));  //string为实际类型参数
console.log(identity("HelloWord"));
  • 类型参数的默认类型也可以引用形式类型参数列表中的其他类型参数,但是只能引用在当前类型参数左侧(前面)定义的类型参数
function identity<T, U = T>(arg: T, a: U): T {
  return arg;
}
console.log(identity<string>("HelloWord", "1"));
console.log(identity<string, number>("HelloWord", 1));

可选类型参数

  • 当形式类型参数设置了默认类型,那么该类型参数是一个可选类型参数
  • 必选类型参数不允许出现在可选类型参数之后

实际类型参数

引用泛型类型时,可以传入一个实际类型参数作为形式类型参数的值,该过程称作泛型的实例化;当显式地传入实际类型参数时,只有必选类型参数是一定要提供的,可选类型参数可以被省略

类型参数类型推论

如果没有明确的指定类型,那么 TypeScript 会依照类型推论(Type Inference)的规则推断出一个类型

function identity<T>(arg: T): T {
  return arg;
}
console.log(identity<string>("HelloWord"));
console.log(identity(1)); //未指定实际类型参数,类型推论推断出实际类型参数为number类型

泛型约束

  • 在泛型的形式类型参数上允许定义一个约束条件,它能够限定类型参数的实际类型的范围。我们将类型参数的约束条件称为泛型约束。
interface Person {
  name: string;
  age: number;
}
interface Tp {
  name: string;
  age: number;
  sex: number;
}

function identity<T extends Person>(arg: T): T {
  let p: T = arg;
  let p1: Person = arg;
  p1 = p;  //p实际类型参数是Person类型的子类型,因此p可以赋值给person
  return arg;
}
console.log(identity<Tp>({ name: "a", age: 32, sex: 0 }));

console.log(identity({ name: "a", age: 32, sex: 0 }));//类型推论得出T类型为{ name: "a", age: 32, sex: 0 }
console.log(identity<{name: string}>({ name: "a"})); //不满足泛型约束
  • 可以同时定义泛型约束和默认类型,但默认类型必须满足泛型约束

  • 约束类型允许引用当前形式类型参数列表中的其他类型参数,但不允许直接或间接地将其自身作为约束类型

//正确引用
<T,U extends T>
<T extends U,U>

//错误引用;直接或间接地将其自身作为约束类型将产生循环引用的编译错误
<T extends T>
<T extends U,U extends T>
基约束

每个类型参数都有一个基约束(Base Constraint),它与是否在形式类型参数上定义了泛型约束无关。类型参数的实际类型一定是其基约束的子类型,规则如下

  • 如果类型参数 T 声明了泛型约束,且泛型约束为另一个类型参数 U,那么类型参数 T 的基约束为类型参数 U
  • 如果类型参数 T 声明了泛型约束,且泛型约束为某一具体类型 Type,那么类型参数 T 的基约束为类型 Type
  • 如果类型参数 T 没有声明泛型约束,那么类型参数 T 的基约束为空对象类型字面量“{}”(等同于<T extends {}>)。除了 undefined 类型和 null 类型外,其他任何类型都可以赋值给空对象类型字面量

泛型函数

函数既能捕获传入参数的类型(类似于方法中的变量参数),又能够使用捕获的传入参数类型用于函数内部或返回值,称为泛型函数,优点是减少代码量,增加复用

  • 调用签名 (X:T):T
function identity<T>(arg: T): T {
  return arg;
}
console.log(identity<string>("HelloWord"));
console.log(identity<string>(false)); //编译错误,类型不正确
console.log(identity("HelloWord")); //未指定实际类型参数,类型推论推断出实际类型参数为string类型
  • 构造签名 new ():T[]
let Dog: { new <T extends string | string[]>(type: T): object } = class<T> {
  private describe;
  constructor(describe: T) {
    this.describe = describe;
  }
};
let dog = new Dog(["cute"]);

注意 :

  • 泛型函数的类型参数是用来关联多个不同值的类型的,如果一个类型参数只在函数签名中出现一次,则说明它与其他值没有关联,因此不需要使用类型参数,直接声明实际类型即可
  • 泛型函数,在未指定实际类型参数得情况下,可根据类型推论推断出实际类型

泛型接口

  • 接口的定义中带有类型参数,那么它是泛型接口;泛型接口定义中,形式类型参数列表紧随接口名之后
  • 引用泛型接口时,必须指定实际类型参数,除非类型参数定义了默认类型
interface ArrayLike<T> {
  readonly length: number;
  readonly [n: number]: T;
}
const numList: ArrayLike<string> = ["a"];

泛型类型别名

  • 若类型别名的定义中带有类型参数,那么它是泛型类型别名;在泛型类型别名定义中,形式类型参数列表紧随类型别名的名字之后
  • 引用泛型类型别名表示的类型时,必须指定实际类型参数,除非类型参数定义了默认类型
//定义值类型可为T类型、undefined、null
type Nullable<T> = T | undefined | null;
const a: Nullable<number> = 2;
const b: Nullable<number> = null;

//使用泛型类型别名定义树形结构
type Tree<T> = {
  id: T;
  value: T;
  children: Tree<T>[] | null;
};
const treeList: Tree<number>[] = [
  {
    id: 1,
    value: 100,
    children: [
      {
        id: 11,
        value: 1001,
        children: null,
      },
    ],
  },
];

泛型类

  • 若类的定义中带有类型参数,那么它是泛型类;若类的定义中带有类型参数,那么它是泛型类
class Animal<T> {
  private describe;
  constructor(describe: T) {
    this.describe = describe;
  }
}
let a = new Animal<string>("cute");
let b = new Animal("cute");
  • 泛型类中的类型参数允许在类的继承语句中使用,即 extends 语句
//继承语句
class Animal<T> {
  private describe;
  constructor(describe: T) {
    this.describe = describe;
  }
}
class Dog<T> extends Animal<T> {
  sex: string = "female";
  constructor(describe: T) {
    super(describe);
  }
}
let a = new Dog<string>("cute");
let b = new Dog("cute");

//接口实现语句中
  • 泛型类型参数不能用于类的静态成员
class Animal<T> {
  private describe;
  static info: T;  //编译出错;Static members cannot reference class type parameters.
  constructor(describe: T) {
    this.describe = describe;
  }
}

最后

码字不易,来个小心心哦!