TypeScript 基础

188 阅读17分钟

主讲:云隐

一、TS 的基础概念

什么是 TS

它是 JS 的一个超集,在原有的语法基础上,添加强类型并切换为基于类的面向对象语言。

对比原理

  • 面向项目:
    • TS:面向解决大型的复杂项目、架构、代码维护复杂场景;
    • JS:脚本化语言,用于面向简单页面场景;
  • 自主检测:
    • TS:编译时,主动发现并纠正错误;
    • JS:运行时,执行报错;
  • 类型检测:
    • TS:强类型语言,支持动态和静态的类型检测;
    • JS:弱类型语言,无静态类型选项;
  • 运行流程:
    • TS:依赖编译,依靠编译打包实现在浏览器端的运行;
    • JS:可直接在浏览器端运行;
  • 复杂特性:
    • TS:模块化、接口、泛型;

安装运行

// 安装
npm install -g typescript
// 查看版本号
tsc -v

// 运行 ts 文件
tsc xxx.ts

// 面试点:所有类型的检测和纠错 - 编译时 => 工程化

面试点: 所有类型的检测和纠错 —— 编译时;

TS 基础类型和语法

  • string:字符串
  • number:数字
  • boolean:布尔
  • nullnull
  • undefinedundefined
  • array:数组
// ES
let isEnable = false;
let className = 'zhaowa';
let classNum = 2;
let u = undefined;
let n = null;
let classArr = ['basic', 'execute'];
// TS
let isEnable: boolean = false;
let className: string = 'zhaowa';
let classNum: number = 2;
let u: undefined = undefined;
let n: null = null;

// 统一方式 & <>方式
let classArr: string[] = ['basic', 'execute'];
let classArr: Array<string> = ['basic', 'execute'];
  • tuple:元组
    • 当访问一个越界的元素会报错;
let tupleType: [string, boolean] = ['basic', false];
console.log(tupleType); // [ 'basic', false ]
console.log(tupleType[0]); // 'basic'
console.log(tupleType[2]); // 当访问一个越界的元素会报错
  • enum:枚举

    • 数字型枚举:默认从 0 开始,依次递增;
    • 字符串类型枚举;
    // 数字型枚举:默认从 0 开始,依次递增
    enum Score {
      BAD, // 0
      NG, // 1
      GOOD, // 2
      PERFECT // 3
    }
    let score: Score = Score.BAD; // 0
    
    // 字符串类型枚举
    enum Score {
      BAD = 'BAD',
      NG = 'NG',
      GOOD = 'GOOD',
      PERFECT = 'PERFECT'
    }
    let score: Score = Score.NG; // 'NG'
    
    • 反向映射:
    // 反向映射
    enum Score {
      BAD, // 0
      NG, // 1
      GOOD, // 2
      PERFECT // 3
    }
    
    let scoreName = Score[0]; // 'BAD'
    let scoreValue = Score['BAD']; // 0
    
    • 异构:
    // 异构
    enum Enum {
      A, // 0
      B, // 1
      C = 'C',
      D = 'D',
      E = 8,
      F // 9
    }
    

    面试题: 异构类型每一项的枚举值 => js 本质实现(手写一个异构枚举的实现)

    let Enum;
    
    (function (Enum) {
      // 正向
      Enum['A'] = 0;
      Enum['B'] = 1;
      Enum['C'] = 'C';
      Enum['D'] = 'D';
      Enum['E'] = 8;
      Enum['F'] = 9;
    
      // 逆向
      Enum[0] = 'A';
      Enum[1] = 'B';
      Enum[8] = 'E';
      Enum[9] = 'F';
    })(Enum || (Enum = {}));
    
  • any:绕过所有类型检查,导致取消类型检测和编译筛查;

// any:绕过所有类型检查 => 类型检测和编译筛查取消
let anyValue: any = 123;

anyValue = 'anyValue';
anyValue = false;
let value1: boolean = anyValue;
  • unknown:绕过赋值检查,但是禁止更改传递;
    • 可以把 unknown 类型的值赋值给 unknown 或者 any 类型,但是不可以赋值给除了 any/unknown 的其它类型;
// unknown:绕过赋值检查 => 禁止更改传递
let unknownValue: unknown;

unknownValue = true;
unknownValue = 123;
unknownValue = 'unknownValue';

let value1: unknown = unknownValue; // OK
// 可以把 unknown 类型的值赋值给 any 类型
let value2: any = unknownValue; // OK

// 不可以赋值
let value3: string = unknownValue; // NOK
let value3: null = unknownValue; // NOK
let value3: undefined = unknownValue; // NOK
  • void:声明返回为空(没有 return);
// void:声明返回为空
function voidFunction(): void {
  console.log('void');
}
  • never:永不能执行完或者永远 error
// never:永不能执行完 or 永远 error
function errorGen(msg: string): never {
  throw new Error(msg);
}

function infiniteLoop(): never {
  while (true) {
    // 业务逻辑
  }
}
  • object / Object / {}:对象
// object:非原始类型
interface ObjectConstructor {
  create(o: object | null): any;
}

const proto = {};

Object.create(proto);
Object.create(null);
Object.create(undefined); // Error

// Object
// Object.prototype 上的属性
interface Object {
  constructor: Function;
  toString(): string;
  toLocaleString(): string;
  valueOf(): Object;
  // ...
}

// 定义了 Object 类属性
interface ObjectConstructor {
  new (value: any): Object;
  readonly prototype: Object;
  // ...
}

// {}:定义空属性对象
const obj = {};

obj.prop = 'props'; // NOK
obj.toString(); // OK

二、接口 - interface

对行为模块的抽象,具体的行为是由类来实现。

接口的定义和使用

赋值的时候,变量的形状必须和接口的形状保持一致。

// 以下接口的使用都不可以

// 1、定义的变量比接口少一些属性
interface Person {
  name: string;
  age: number;
}

let tom: Person = {
  name: 'Tom'
};

// 2、定义的变量比接口多一些属性
interface Person {
  name: string;
  age: number;
}

let tom: Person = {
  name: 'Tom',
  age: 25,
  gender: 'male'
};

可选属性 - ?

interface Person {
  name: string;
  age?: number;
}

let tom: Person = {
  name: 'Tom'
};

let tom2: Person = {
  name: 'Tom',
  age: 22
};

任意属性

interface Class {
  readonly name: string;
  time?: number;

  [propName: string]: any; // 任意可添加属性
}

const c1: Class = { name: 'JS' };
const c2: Class = { name: 'browser', time: 1 };
const c3: Class = { name: 'ts', level: 1 };

只读属性 - readonly

interface Person {
  readonly id: number; // id 是只读的,不能被改变
  name: string;
  age?: number;
  [propName: string]: any;
}

let tom: Person = {
  id: 89757,
  name: 'Tom',
  gender: 'male'
};

tom.id = 9527; // 报错

注意,只读的约束存在于第一次给对象赋值的时候,而不是第一次给只读属性赋值的时候。

interface Person {
  readonly id: number; // id 为只读属性,没有给 id 赋值
  name: string;
  age?: number;
  [propName: string]: any;
}

let tom: Person = {
  name: 'Tom',
  gender: 'male'
};

tom.id = 89757; // 报错,只读属性不能再次被改变
// 描述对象内容
interface Class {
  name: string;
  time: number;
}

let zhaowa: Class = {
  name: 'typescript',
  time: 2
};

// readonly 只读
// ? 可选属性
interface Class {
  readonly name: string;
  time: number;
  isEnd?: false;
}

面试题:JS 的引用类型操作不同 < = > const

let arr: number[] = [1, 2, 3, 4];

// ReadonlyArray<T> 是一个真正的只读数组,它没有任何可以更改数组的方法
let ro: ReadonlyArray<number> = arr; // ro 是只读类型的

// readonly 声明的数据,是不可以被改变的
ro[0] = 12; // 赋值 - Error
ro.push(5); // 增加 - Error
ro.length = 10; // 长度改写 - Error

// 这里可以理解为跟 const 一样,arr 数组中的数据可以改,但是不能改变引用的地址
arr = ro; // 覆盖 - Error

三、交叉类型 - &

合并

// 合并
interface A {
  inner: D;
}
interface B {
  inner: E;
}
interface C {
  inner: F;
}

interface D {
  d: boolean;
}
interface E {
  e: string;
}
interface F {
  f: number;
}

// 声明一个新的类型(ABC),类型是 接口A、接口B、接口C 的合并
type ABC = A & B & C; // type:定义一个类型,&:类型合并

// 接口的交叉合并,同类型的放在一起,不同类型的平铺开
let abc: ABC = {
  inner: {
    d: false,
    e: 'className',
    f: 5
  }
};

// 好处:在接口复杂,嵌套比较深的情况下,可以拆分开来声明,最后做合并

合并冲突

// 合并冲突
interface A {
  c: string;
  d: string;
}
interface B {
  c: number;
  e: string;
}

type AB = A & B;

let ab: AB = {
  c: 'c', // 不能将类型“string”分配给类型“never”
  d: 'd',
  e: 'e'
};
// 合并的关系是 '且' => c - never

四、断言

断言是类型的声明和转换(和编译器做了一个告知交流)。

  • 特点:编译时作用;

声明形式

  • 两种声明形式:尖括号形式、as 声明形式;
    • 两种形式是等价的;
    • 但是,在 TypeScript 里使用 JSX 时,只有 as 语法断言是被允许的。
// 1、尖括号形式
let anyValue: any = 'hi zhaowa';

// 执行到 anyValue 的时候,此时的 anyValue 为字符串类型
let anyLength: number = (<string>anyValue).length;

// 2、as 声明
let anyValue: any = 'hi zhaowa';

let anyLength: number = (anyValue as string).length;

非空断言

  • 非空断言:!,断言操作对象是非 null 和非 undefined 类型;
function sayHello(name: string | undefined) {
  // let sname: string = name; // 这种写法会报错

  // 可以简单加个条件判断
  // let sname: string;
  // if (name) {
  //   sname = name;
  // }

  // 或者使用非空断言操作符 !
  let sname: string = name!;
}
// 非空判断 - 只确定不是空
type ClassTime = () => number;

const start = (ClassTime: ClassTime | undefined) => {
  // 业务逻辑
  // if (额外判断逻辑) {
  let time = ClassTime!(); // 具体类型待定,但是非空确认
  // }
};

面试题:

const tsClass: number | undefined = undefined;

// 在声明变量时,只是打声招呼说 tsClass 是非空的,在编译时会不处理这里,但是实际转义完后这里还是会存在 undefined 情况的
// 所以,【不建议】在赋值的时候做非空断言
// 非空断言尽量在逻辑功能块里使用,而不是在赋值的时候直接非空断言
const zhaowa: number = tsClass!;

console.log(zhaowa); // undefined

// 严格模式下,会被转义成
('use strict');
const tsClass = undefined;
const zhaowa = tsClass;
console.log(zhaowa); // undefined

肯定断言

肯定化保证赋值。

let score: number;
startClass();

console.log('' + score); // 使用前赋值,报错 - 在赋值前使用了变量“score”

function startClass() {
  score = 5;
}

// 肯定断言 - 肯定化保证赋值
let score!: number; // 提前告知,score 是有值的
startClass();

console.log('' + score);

function startClass() {
  score = 5;
}

五、类型守卫 - 语法规范范围内,额外的确认

多态 - 多种状态(多种类型)

in - 定义属性场景下内容的确认

  • in 的是【属性】
// in - 定义属性场景下内容的确认
interface Teacher {
  name: string;
  courses: string[];
}
interface Student {
  name: string;
  startTime: Date;
}

type PeopleClass = Teacher | Student;

function startCourse(cls: PeopleClass) {
  // 使用类型守卫 in 判断
  if ('courses' in cls) {
    console.log('Courses: ' + cls.courses);
  }
  if ('startTime' in cls) {
    console.log('startTime: ' + cls.startTime);
  }
}

startCourse({
  name: 'teacher',
  // courses: ['123']
  startTime: new Date('2022-02-11')
});

typeof / instanceof - 类型分类场景下的身份确认

typeof 的使用

// typeof 的使用
function className(name: string, score: string | number) {
  // 使用 typeof 判断类型
  if (typeof score === 'number') {
    return 'teacher: ' + name + ', ' + score + ' 分';
  }
  if (typeof score === 'string') {
    return 'student: ' + name + ', ' + score + ' 分';
  }
}

const classValue1 = className('tom', 80);
console.log(classValue1); // teacher: tom, 80 分

const classValue2 = className('tom', '90');
console.log(classValue2); // student: tom, 90 分

instanceof 的使用

  • instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上;
class Teacher {
  constructor() {
    this.name = 'teacher';
    this.courses = ['js', 'ts'];
  }
}

const t = new Teacher();
console.log(t); // Teacher { name: 'teacher', courses: [ 'js', 'ts' ] }
console.log(t.__proto__ === Teacher.prototype); // true
// instanceof 的使用
interface Teacher {
  name: string;
  courses: string[];
}
interface Student {
  name: string;
  startTime: Date;
}

class MyTeacher implements Teacher {
  name: string;
  courses: string[];
  constructor(name: string, courses: string[]) {
    this.name = name;
    this.courses = courses;
  }
}

class MyStudent implements Student {
  name: string;
  startTime: Date;
  constructor(name: string, startTime: Date) {
    this.name = name;
    this.startTime = startTime;
  }
}

type PeopleClass = Teacher | Student;

const getName = (cls: PeopleClass) => {
  if (cls instanceof MyTeacher) {
    return cls.courses;
  }
  if (cls instanceof MyStudent) {
    return cls.startTime;
  }
};

let t1 = new MyTeacher('tom', ['js', 'ts']);
const value1 = getName(t1);
console.log(value1); // [ 'js', 'ts' ]

let s1 = new MyStudent('jack', new Date());
const value2 = getName(s1);
console.log(value2); // 2022-04-15T18:25:39.531Z

自定义类型

  • 自定义守卫通过 {形参} is {类型} 的语法结构;
// 自定义类型
// cls 是 Teacher 类型 或者 Student 类型,判断当前函数是不是 Teacher
const isTeacher = function (cls: Teacher | Student): cls is Teacher {
  return 'courses' in cls;
};

const getName = (cls: Teacher | Student) => {
  if (isTeacher(cls)) {
    return cls.courses;
  }
};

const value = getName({
  name: 'teacher',
  courses: ['js', 'ts']
});

console.log(value); // [ 'js', 'ts' ]

六、 TS 进阶

函数重载

  • 函数重载主要承载的功能是 复用 —— 对实际数据的类型做不同的匹配;
// Combinable 复合类型
type Combinable = number | string;

class Course {
  start(name: number, score: number): number;
  start(name: string, score: string): string;
  start(name: string, score: number): string;
  start(name: number, score: string): string;
  start(name: Combinable, score: Combinable) {
    if (typeof name === 'number' && typeof score === 'number') {
      return score;
    }

    if (typeof name === 'string' && typeof score === 'number') {
      return 'student: ' + name + ', ' + score + ' 分';
    }
  }
}

const course = new Course();
const value = course.start('云隐', 5);
console.log(value); // student: 云隐, 5 分

泛型 - 复用

  • 让模块可以支持多种类型数据 —— 让类型声明和值一样,可以被赋值和传递;
// 返回值类型声明为 string
function startClass<T, U>(name: T, score: U): string {
  return `姓名:${name},分数:${score}`;
}

// 或者使用 as 断言
function startClass<T, U>(name: T, score: U): T {
  return (name + String(score)) as unknown as T;
}

const value = startClass<String, Number>('云隐', 5);
console.log(value); // 云隐5

// T、U、K - 键值
// V - 值
// E - 节点、元素

装饰器 - decorator

装饰器分类

  • 常见的装饰器有:类装饰器属性装饰器方法装饰器参数装饰器
  • 装饰器的写法:普通装饰器(无法传参)装饰器工厂(可传参)

tsconfig.json 配置

{
  "compilerOptions": {
    "target": "ES5",
    "experimentalDecorators": true
  }
}

类装饰器

类装饰器在类声明之前被声明(紧靠着类声明)。类装饰器应用于构造函数,可以用来监视、修改或者替换类定义。

  • 参数:类的构造函数(唯一的参数);
  • 类装饰器:普通装饰器(无法传参)
function logClass(params: Function): void {
  // params 就是当前类
  console.log(params);

  params.prototype.apiUrl = '动态扩展的属性';

  params.prototype.run = function (): void {
    console.log('我是一个 run 方法');
  };
}

@logClass
class HttpClient {
  constructor() {}
  getData() {}
}

const http: any = new HttpClient();
console.log(http.apiUrl);
http.run();
  • 类装饰器:装饰器工厂(可传参)
function logClass(params: string): Function {
  return function (target: Function): void {
    // params:传入的参数
    // target:当前类
    // console.log(target); // [class HttpClient]
    // console.log(params); // http://www.baidu.com/api

    target.prototype.apiUrl = params;
  };
}

@logClass('http://www.baidu.com/api')
class HttpClient {
  constructor() {}
  getData() {}
}

let http: any = new HttpClient();
console.log(http.apiUrl);
  • 类装饰器:重载构造函数
    • 下面是一个重载构造函数的例子;
    • 类装饰器表达式会在运行时当作函数被调用,类的构造函数作为其唯一的参数;
    • 如果类装饰器返回一个值,它会使用提供的构造函数来替换类的声明
function logClass(target: any) {
  return class extends target {
    apiUrl = '我是修改后的数据';

    getData(): void {
      this.apiUrl += '!';
      console.log(this.apiUrl);
    }
  };
}

@logClass
class HttpClient {
  public apiUrl: string | undefined;

  constructor() {
    this.apiUrl = '我是构造函数里面的 apiUrl';
  }

  getData(): void {
    console.log(this.apiUrl);
  }
}

const http = new HttpClient();
http.getData(); // 我是修改后的数据!

属性装饰器

属性装饰器表达式会在运行时当做函数被调用,传入下列 2 个参数:

  • 对于 静态成员 来说是类的 构造函数,对于 实例成员 是类的 原型对象
  • 成员的名字;
function logProperty(params: string) {
  return function (target: any, attrName: string) {
    console.log(params); // http://www.baidu.com
    console.log(target); // 静态成员 - 构造函数;实例成员 - 原型对象 {}
    // 此处是 类的原型对象,相当于 xxx.prototype
    console.log(attrName); // 成员的名字:apiUrl

    target[attrName] = params;
  };
}

class HttpClient {
  @logProperty('http://www.baidu.com')
  public apiUrl: string | undefined;

  constructor() {}

  getData(): void {
    console.log(this.apiUrl);
  }
}

const http = new HttpClient();
http.getData(); // http://www.baidu.com

方法装饰器

方法装饰器会被应用到方法的属性描述符上,可以用来监视、修改或者替换方法的定义。

方法装饰器会在运行时传入下列 3 个参数:

  • 对于 静态成员 来说是类的 构造函数,对于 实例成员 是类的 原型对象
  • 成员的名字;
  • 成员的属性描述符;
// 方法装饰器1 - 扩展当前类的属性和方法
function get(params: string) {
  return function (target: any, methodName: string, desc: any) {
    console.log(params); // http://www.baidu.com
    console.log(target); // 静态成员 - 构造函数;实例成员 - 原型对象 {}
    // 此处是 类的原型对象,相当于 xxx.prototype
    console.log(methodName); // 成员的名字:getData
    console.log(desc); // 属性描述符
    /* 
      {
        value: [Function: getData],
        writable: true,
        enumerable: false,
        configurable: true
      }
    */

    // 扩展当前类的属性和方法
    target.apiUrl = 'xxx';

    target.run = function (): void {
      console.log('run');
    };
  };
}

class HttpClient {
  public url: string | undefined;

  constructor() {}

  @get('http://www.baidu.com')
  getData(): void {
    console.log(this.url);
  }
}

const http: any = new HttpClient();
console.log(http.apiUrl); // xxx
http.run(); // run
// 方法装饰器2 - 修改装饰的方法
function get(params: string) {
  return function (target: any, methodName: string, desc: any) {
    console.log(params); // http://www.baidu.com
    console.log(target); // 静态成员 - 构造函数;实例成员 - 原型对象 {}
    // 此处是 类的原型对象,相当于 xxx.prototype
    console.log(methodName); // 成员的名字:getData
    console.log(desc); // 属性描述符
    /* 
      {
        value: [Function: getData],
        writable: true,
        enumerable: false,
        configurable: true
      }
    */

    // 修改装饰的方法 - 可以通过 desc 中的 value 修改
    // 实现目标:把装饰器方法里面传入的所有参数改为 string 类型

    // 1、保存当前的方法
    const oldMethod = desc.value;

    // 2、修改原始的方法
    desc.value = function (...args: any[]) {
      console.log('args ==== ', args); // [ 123, 'xxx' ]

      args = args.map(value => String(value));

      // 3、调用原始的方法
      oldMethod.apply(this, args);
    };
  };
}

class HttpClient {
  public url: string | undefined;

  constructor() {}

  @get('http://www.baidu.com')
  getData(...args: any[]): void {
    console.log(args);
    console.log('我是 getData 里面的方法');
  }
}

const http: any = new HttpClient();
http.getData(123, 'xxx');
/* 
  输出结果:
    [ '123', 'xxx' ]
    我是 getData 里面的方法
*/

方法参数装饰器

参数装饰器表达式在运行时当作函数被调用,可以使用参数装饰器为类的原型增加一些元素数据,传入下列 3 个参数:

  • 对于 静态成员 来说是类的 构造函数,对于 实例成员 是类的 原型对象
  • 成员的名字;
  • 参数在函数参数列表中的索引;
function logParams(params: any) {
  return function (target: any, methodName: any, paramsIndex: any) {
    console.log(params); // xxxxx
    console.log(target); // {}
    console.log(methodName); // getData
    console.log(paramsIndex); // 0

    target.apiUrl = params;
  };
}
class HttpClient {
  public url: string | undefined;

  constructor() {}

  getData(@logParams('xxxxx') uuid: any) {
    console.log('我是 getData 里面的方法');
    console.log(uuid); // 123456
  }
}

let http: any = new HttpClient();
http.getData(123456);
console.log(http.apiUrl); // xxxxx

装饰器的执行顺序

  • 属性装饰器 => 方法装饰器 => 参数装饰器 => 类装饰器
  • 如果有多个同样的装饰器,它会先执行后面的;
// 类装饰器
function logClass1(params: any) {
  return function (target: any) {
    console.log('类装饰器1');
  };
}

function logClass2(params: any) {
  return function (target: any) {
    console.log('类装饰器2');
  };
}

// 属性装饰器
function logProperty(params?: string) {
  return function (target: any, attrName: any) {
    console.log('属性装饰器');
  };
}

// 方法装饰器
function logMethod(params?: any) {
  return function (target: any, methodName: any, desc: any) {
    console.log('方法装饰器');
  };
}

// 参数装饰器
function logParams1(params?: any) {
  return function (target: any, methodName: any, paramsIndex: any) {
    console.log('参数装饰器1');
  };
}

function logParams2(params?: any) {
  return function (target: any, methodName: any, paramsIndex: any) {
    console.log('参数装饰器2');
  };
}

@logClass1('xxx-1')
@logClass2('xxx-2')
class HttpClient {
  @logProperty()
  public url: string | undefined;

  constructor() {}

  @logMethod()
  getData() {
    return true;
  }

  setData(@logParams1() attr1: any, @logParams2() attr2: any) {
    console.log('我是 getData 里面的方法');
  }
}

let http: any = new HttpClient();

/* 
  输出结果:
    属性装饰器
    方法装饰器
    参数装饰器2
    参数装饰器1
    类装饰器2
    类装饰器1
*/

补充知识点

TS 中类的定义

class Person {
  name: string; // 属性,前面省略了 public 关键字

  constructor(name: string) {
    // 构造函数,实例化类的时候触发的方法
    this.name = name;
  }

  getName(): string {
    return this.name;
  }

  setName(name: string) {
    this.name = name;
  }

  run(): void {
    console.log(`${this.name}在运动`);
  }
}

const p = new Person('张三');
p.run(); // 张三在运动
console.log(p.getName()); // 张三

p.setName('李四');
console.log(p.getName()); // 李四

TS 中实现继承,使用关键字 extends、super

class Person {
  name: string;

  constructor(name: string) {
    this.name = name;
  }

  run(): void {
    console.log(`${this.name}在运动`);
  }
}

const p = new Person('张三');
p.run(); // 张三在运动

class Web extends Person {
  constructor(name: string) {
    super(name); // 初始化父类的构造函数
  }
}

const w = new Web('李四');
w.run(); // 李四在运动

TS 中继承的探讨:父类的方法和子类的方法一致时,使用子类;

class Person {
  name: string;

  constructor(name: string) {
    this.name = name;
  }

  run(): void {
    console.log(`${this.name}在运动`);
  }
}

const p = new Person('张三');
p.run(); // 张三在运动

class Web extends Person {
  constructor(name: string) {
    super(name); // 初始化父类的构造函数
  }

  run(): void {
    console.log(`${this.name}在运动 - 子类`);
  }
  work(): void {
    console.log(`${this.name}在工作`);
  }
}

const w = new Web('李四');
w.run(); // 李四在运动 - 子类

类里面的修饰符

TypeScript 里面定义属性的时候给我们提供了三种修饰符:

  • public:公有,在类里面、子类和类外面可以访问;
  • protected:保护类型,在类里面和子类可以访问,在类外面无法访问;
  • private:私有,只有在类里面可以访问,子类和类外面无法访问;
  • 属性如果不加修饰符,默认就是公有(public);
class Person {
  public name: string; // 公有属性
  protected age: number; // 保护类型
  private sex: string; // 私有属性

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

  run(): string {
    return `${this.name}在运动`;
  }

  getAge(): void {
    console.log(`父类-年龄:${this.age}`);
  }

  getSex(): void {
    console.log(`父类-性别:${this.sex}`);
  }
}

class Web extends Person {
  constructor(name: string, age: number, sex: string) {
    super(name, age, sex); // 初始化父类的构造函数
  }

  work(): void {
    console.log(`${this.name}在工作`);
  }

  run(): string {
    return `${this.name}在运动 - 子类`;
  }

  getAge(): void {
    console.log(`子类-年龄:${this.age}`);
  }

  getSex(): void {
    // console.log(`子类-性别:${this.sex}`); // 报错,属性“sex”为私有属性,只能在类“Person”中访问
  }
}

let w = new Web('李四', 22, '男');
w.work(); // 李四在工作,【公有属性在子类可以访问】
w.getAge(); // 子类-年龄:22,【保护类型在子类可以访问】
w.getSex();

// 类外面可以访问公有属性
let p = new Person('哈哈', 22, '女');
console.log(p.name); // 哈哈
p.getAge(); // 父类-年龄:22,【保护类型在父类可以访问】
p.getSex(); // 父类-性别:女,【私有属性在父类可以访问】

// 类外面无法访问保护类型
// console.log(p.age); // 报错,属性“age”受保护,只能在类“Person”及其子类中访问
// console.log(p.sex); // 报错,属性“sex”为私有属性,只能在类“Person”中访问

静态属性和静态方法,使用 static 修饰

class Person {
  public name: string;
  public age: number = 20;

  // 静态属性
  static sex: string = '男';

  constructor(name: string) {
    this.name = name;
  }

  // 实例方法
  run(): void {
    console.log(`${this.name}在运动`);
  }

  work(): void {
    console.log(`${this.name}在工作`);
  }

  // 静态方法 - 静态方法里面无法直接调用类里面的属性
  static print() {
    // console.log(`我是公有属性:${this.name}`); // 报错
    console.log(`我是静态属性:${this.sex}`);
  }
}
var p = new Person('张三');
p.run(); // 张三 在运动

// 直接通过【类型.xxx】调用静态方法
Person.print(); // 我是静态属性:男

多态

  • 父类定义一个方法不去实现,让继承它的子类去实现,每一个子类有不同的表现;
  • 多态属于继承;
class Animal {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  eat(): void {
    console.log('父类 - 吃的方法');
  }
}

class Dog extends Animal {
  constructor(name: string) {
    super(name);
  }

  eat(): void {
    console.log(`${this.name}吃肉`);
  }
}

class Cat extends Animal {
  constructor(name: string) {
    super(name);
  }

  eat(): void {
    console.log(`${this.name}吃老鼠`);
  }
}

抽象类 - abstract

  • TypeScript 中的抽象类,它是提供其他类继承的基类,不能直接被实例化
  • abstract 关键字定义抽象类和抽象方法,抽象类中的抽象方法不包含具体实现并且必须在派生类中实现;
  • abstract 抽象方法只能方法抽象类里面;
  • 抽象类和抽象方法用来定义标准;
    • 例如:Animal 这个类要求它的子类必须包含 eat 方法;
// 使用 abstract 定义抽象类
abstract class Animal {
  public name: string;

  constructor(name: string) {
    this.name = name;
  }

  // 使用 abstract 定义抽象方法,抽象方法必须要在抽象类中
  abstract eat(): void;

  run(): void {
    console.log('其他方法在子类中可以不实现');
  }
}

// const a = new Animal(); // 报错,无法创建抽象类的实例。

class Dog extends Animal {
  constructor(name: string) {
    super(name);
  }

  // 抽象类的子类必须实现抽象类里面的抽象方法
  eat(): void {
    console.log(`${this.name}吃粮食`);
  }
}

const dog = new Dog('二哈');
dog.eat(); // 二哈吃粮食

class Cat extends Animal {
  constructor(name: string) {
    super(name);
  }

  eat(): void {
    console.log(`${this.name}吃老鼠`);
  }
}

const cat = new Cat('小花');
cat.eat(); // 小花吃老鼠

联合类型

联合类型(Union Types)表示取值可以为多种类型中的一种。

联合类型使用 | 分隔每个类型。

let myFavoriteNumber: string | number;

function getLength(something: string | number): number {
  return something.length; // 报错,length 不是 string 和 number 的共有属性,所以会报错
}

function getLength(something: string | number): string {
  return something.toString(); // OK
}

接口

函数类型

为了使用接口表示函数类型,我们需要给接口定义一个调用签名。 它就像是一个只有参数列表和返回值类型的函数定义。参数列表里的每个参数都需要名字和类型。

// 定义函数型接口
interface SearchFunc {
  (source: string, subString: string): boolean;
}

// 使用
let mySearch: SearchFunc;
mySearch = function (source: string, subString: string) {
  let result = source.search(subString);
  return result > -1;
};

// 或者
let mySearch: SearchFunc;
mySearch = function (src: string, sub: string): boolean {
  let result = src.search(sub);
  return result > -1;
};

可索引的类型

// 对数组的约束
interface StringArray {
  [index: number]: string;
}

let myArray: StringArray;
myArray = ['Bob', 'Fred'];

let myStr: string = myArray[0]; // 'Bob'

// 对对象的约束
interface UserObj {
  [index: string]: string;
}

const obj: UserObj = {
  name: '张三'
};

类类型 - 对类的约束,和抽象类有点相似

interface Animal {
  name: string;
  eat(str: string): void;
}

// implements 实现接口
class Dog implements Animal {
  name: string;

  constructor(name: string) {
    this.name = name;
  }

  eat(str: string): void {
    console.log(`${this.name}${str}`);
  }
}

const dog = new Dog('二哈');
dog.eat('肉'); // 二哈吃肉

class Cat implements Animal {
  name: string;

  constructor(name: string) {
    this.name = name;
  }

  eat(str: string): void {
    console.log(`${this.name}${str}`);
  }
}

const cat = new Cat('小花');
cat.eat('粮食'); // 小花吃粮食

接口扩展:接口可以继承接口

interface Animal {
  eat(str: string): void;
}

// 接口 Person 继承了 接口 Animal
interface Person extends Animal {
  work(): void;
}

class Web implements Person {
  public name: string;

  constructor(name: string) {
    this.name = name;
  }

  work(): void {
    console.log(`${this.name}在加班`);
  }

  eat(str: string): void {
    console.log(`${this.name}${str}`);
  }
}

const w = new Web('程序员');
w.work(); // 程序员在加班
w.eat('夜宵'); // 程序员吃夜宵