TS使用正确姿势(一)

117 阅读6分钟

什么是TS

2012年,Microsoft 首次发布公开版本的 TypeScript 。TypeScript 在 JavaScript 的基础上增加静态类型定义和编译时检查,通过编译,可以将 TypeScript 转译为 JavaScript

TypeScript 实际上是 JavaScript 的超集,编译后的 TypeScript 就是去掉类型检查和其他限定的 JavaScript,而在使用 TypeScript 编辑代码时,编译器和 IDE 的编辑器会检查这些限定

除了静态类型的定义和检查外,TypeScript 还引入了类、接口、模块、装饰器、命名空间等特性(ES6中也实现了部分),这些都是 JavaScript 想要胜任大型软件工程项目所急需的特性。

为何使用TS

  • 供很好的的拼写检查和智能提示
  • 方便快捷的代码重构
  • 明确定义函数的参数类型和函数重载

TS常用方法

1. 数据类型

  • boolean
  • number
  • string
  • array
  • tuple

元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同,
eg. let x: [string, number];, 当访问跨界原素,会使用联合类型,x[3] = 'world'; // OK, 字符串可以赋值给(string | number)类型

  • enum

枚举主要用途是,可以为一组数值赋予友好的名字,另外一个便利是能通过key得到value或通过value获取key enum Color {Red = 1, Green, Blue}, Color.Green, Color[2]都能访问

  • any
  • void

没有任何类型,eg.函数没有返回值

function warnUser(): void {
    alert("This is my warning message");
}
  • null, undefined
  • never

表示的是那些永不存在的值的类型。 例如,never类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型;

2. 类型断言:告诉编译器值类型

使用方式:

方式1:尖括号(使用eslint报错,与jsx语法冲突)

const { date, externalId, channel } = this.$route.query;
this.externalId = <string>externalId || null;

方式2:as(推荐)

this.channel = channel as string || '';

3. interface

定义入参,出参

interface SquareConfig {
  color?: string;
  width?: number;
}

function createSquare(config: SquareConfig): {color: string; area: number; readonly name: string;} {
  let newSquare = {color: "white", area: 100};
  if (config.color) {
    newSquare.color = config.color;
  }
  if (config.width) {
    newSquare.area = config.width * config.width;
  }
  newSquare.name = '123';
  return newSquare;
}

let mySquare = createSquare({color: "black"});
mySquare.name = '张三' // 提示不能赋值给只读变量:Attempt to assign to const or readonly variable 
  • “?”可以表示可选属性
  • 只会对声明的属性进行检查,传入额外的属性不会被检查(对象字面量除外,可以通过断言绕开检查) 参见如下示例:
  • readonly表示只读属性,初始化后不可再赋值
  • 任意属性设置[propName: string]: any
// 正常赋值使用,不对额外属性进行检查
function createSquare(config: SquareConfig): {color: string; area: number} {
  const newSquare = {color: 'white', area: 100};
  if (config.color) {
    newSquare.color = config.color;
  }
  if (config.width) {
    newSquare.area = config.width * config.width;
  }
  return newSquare;
}
const options = {width: 100, color: 'yell', test: 123};

createSquare(options);

// 对象字面量形式报错:TS2345: Argument of type '{ width: number; color:  
// string; test: number; }' is not assignable to parameter of type   
// 'SquareConfig'.   Object literal may only specify known properties,  
// and 'test' does not exist in type 'SquareConfig'.
function createSquare(config: SquareConfig): {color: string; area: number} {
  const newSquare = {color: 'white', area: 100};
  if (config.color) {
    newSquare.color = config.color;
  }
  if (config.width) {
    newSquare.area = config.width * config.width;
  }
  return newSquare;
}
// const options = {width: 100, color: 'yell', test: 123};
createSquare({width: 100, color: 'yell', test: 123});

定义函数类型

interface SearchFunc {
  (source: string, subString: string): boolean;
}
let mySearch: SearchFunc;
mySearch = function (source, subString) {
  const pos = source.search(subString);
  return pos > -1;
};
  • 定义函数类型,不校验参数名,但会校验入参,出参类型

image.png

类类型

实现接口

interface ClockInterface {
  currentTime: Date;
  setTime(d: Date);
}
class Clock implements ClockInterface {
  currentTime: Date = new Date();
  setTime(d: Date) {
    this.currentTime = d;
  }
}

接口描述了类的公共部分,而不是公共和私有两部分。 它不会帮你检查类是否具有某些私有成员。

继承接口 extends

interface Shape {
    color: string;
}

interface PenStroke {
    penWidth: number;
}

interface Square extends Shape, PenStroke {
    sideLength: number;
}

let square = <Square>{};
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;

混合类型

一个对象可以同时作为函数类型和对象类型使用,如下:

interface ClockInterface {
  (start: number): string;
  currentTime: Date;
  setTime(d: Date);
}
function Clock (): ClockInterface {
  const clock = function (start) {
    // todo
  } as ClockInterface;
  clock.currentTime = new Date();
  clock.setTime = function (d) {
    this.currentTime = d;
  };
  return clock;
}

3.类

继承

class Animal {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  move(distance: number = 0) {
    console.log(`${this.name} moved ${distance}m.`);
  }
}

class Cat extends Animal {
  // eslint-disable-next-line no-useless-constructor
  constructor(name) {
    super(name);
  }
  move(distance = 5) {
    console.log('Cat move...');
    super.move(distance);
  }
}

const sam = new Cat('little sam');
sam.move();

类从基类继承属性和方法,Cat派生自Amnimal基类,Cat又称作派生类,通常,派生类又被称为子类,基类又被称为超类
在派生类的构造函数访问this之前,一定要调用super, 这个是typescript强制的一条规则。

public, private, protected修饰符

public: 公有地,任何位置均可访问,Typescript里,成员默认为public.

private: 不能在声明它的类的外部访问,表示私有的。

protected: 只能在声明的类内部及派生类中访问

存储器:get/set

class Rabbit {
  readonly secret: string = '';
  private _name: string = 'bob smith';
  constructor(secret) {
    this.secret = secret;
  }
  get fullName(): string {
    return this._name;
  }
  set fullName(name: string) {
    if (this.secret === 'pwd') {
      this._name = name;
    }
  }
}

const rb = new Rabbit('123');
rb.fullName = 'lisa'; // 无效
console.log(rb.fullName); // bob smith

注意,只有get没有set的存储器会被视为readonly属性

静态属性static

我们可以通过static创建类的静态属性,它存在于类本身,而不是实例上,每个实例想要访问静态属性,只能通过父类来访问,而不是this.

class Rabbit {
  static secret: string = '';
  private _name: string = 'bob smith';
  constructor(secret) {
    Rabbit.secret = secret;
  }
  get fullName(): string {
    return this._name;
  }
  set fullName(name: string) {
    if (Rabbit.secret === 'pwd') {
      this._name = name;
    }
  }
}

抽象类

抽象类作为其他派生类的基类使用,一般不直接实例化。抽象类可以包含具体实现细节。abstract可以定义抽象类和在抽象类内部定义抽象方法。

abstract class Department {

    constructor(public name: string) {
    }

    printName(): void {
        console.log('Department name: ' + this.name);
    }

    abstract printMeeting(): void; // 必须在派生类中实现
}

class AccountingDepartment extends Department {

    constructor() {
        super('Accounting and Auditing'); // 在派生类的构造函数中必须调用 super()
    }

    printMeeting(): void {
        console.log('The Accounting Department meets each Monday at 10am.');
    }

    generateReports(): void {
        console.log('Generating accounting reports...');
    }
}

let department: Department; // 允许创建一个对抽象类型的引用
department = new Department(); // 错误: 不能创建一个抽象类的实例
department = new AccountingDepartment(); // 允许对一个抽象子类进行实例化和赋值
department.printName();
department.printMeeting();
department.generateReports(); // 错误: 方法在声明的抽象类中不存在

高级技巧:

  • let greeter: Greeter,意思是Greeter类的实例的类型是Greeter
  • let greeterMaker: typeof Greeter = Greeter;, typeof Greeter,意思是取Greeter类的类型,而不是实例的类型
  • 把类当做接口使用: 类定义会创建两个东西:类的实例类型和一个构造函数。 因为类可以创建出类型,所以你能够在允许使用接口的地方使用类。
class Point {
    x: number;
    y: number;
}

interface Point3d extends Point {
    z: number;
}

let point3d: Point3d = {x: 1, y: 2, z: 3};

4. 函数

类型定义

常用函数类型定义

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

TS能根据返回语句自动推断出返回值类型,因此我们通常省略它。

完整定义:

const add: (x: number, y: number) => number = (x, y) => x + y;

重载

为同一函数提供多种类型定义进行函数重载,编辑器会根据列表处理函数调用

function bridge(name: string, params: {[propsName: string]: any}, shouldWait?: boolean): Promise<any>;
function bridge(name: string, shouldWait: boolean, ...rest): Promise<any>;
function bridge (name, params, shouldWait) {
  if (typeof params === 'boolean') {
    return bridge(name, {}, params);
  } else {
    if (shouldWait) {
      // todo
    }
    return new Promise((resolve, reject) => {
      const data = {};
      // 逻辑处理 ....
      resolve(data);
    });
  }
}
bridge('showTitleBarV2', {});
bridge('showTitleBarV2', {}, true);
bridge('showTitleBarV2', true);

编译器会查找重载列表,尝试使用第一个重载定义。 如果匹配的话就使用这个。 因此,在定义重载的时候,一定要把最精确的定义放在最前面。

5. 泛型

定义

当我们要实现一个函数,将入参作为返回值返回,此时入参,出参类型一致,但类型未知,则可使用泛型:类型变量来定义,如下:

// demo1:
function identity<T>(arg: T): T {
  return arg;
}

// demo2:
function bridge<T>(name: string, params: T, shouldWait?: boolean): Promise<any>;

我们给identity添加了类型变量T。 T帮助我们捕获用户传入的类型(比如:number),之后我们就可以使用这个类型。 之后我们再次使用了T当做返回值类型。

我们把这个版本的identity函数叫做泛型,因为它可以适用于多个类型

使用泛型函数:
方式一:

const output = identity<string>('myString'); // type of output will be 'string'

方式二:(更常用, 利用类型推论,编译器会自动识别 T 类型)

const output1 = identity('myString');

泛型变量

function loggingIdentity<T>(arg: Array<T>): Array<T> {
  console.log(arg.length); // Array has a .length, so no more error
  return arg;
}
loggingIdentity([1, '1']);

泛型接口

方式1:

interface GenericIdentityFn {
    <T>(arg: T): T;
}

function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: GenericIdentityFn = identity;

方式2: 把泛型参数T当作整个接口GenericIdentityFn的一个参数。 这样就能清楚的知道使用的具体是哪个泛型类型

interface GenericIdentityFn<T> {
    (arg: T): T;
}

function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: GenericIdentityFn<number> = identity;

泛型类

泛型类看上去与泛型接口差不多。 泛型类使用(<>)括起泛型类型,跟在类名后面。

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

泛型约束

泛型变量可以通过extends关键字实现约束

interface ParamsInterface {
  [propName: string]: string|number;
}
function bridge(name: string, shouldWait: boolean, ...rest): Promise<any>;
function bridge<T extends ParamsInterface>(name: string, params: T, shouldWait?: boolean): Promise<any>;
function bridge (name, params, shouldWait) {
  if (typeof params === 'boolean') {
    return bridge(name, {}, params);
  } else {
    if (shouldWait) {
      // todo
    }
    return new Promise((resolve, reject) => {
      const data = {};
      // 逻辑处理 ....
      resolve(data);
    });
  }
}
bridge('showTitleBarV2', {});
bridge('showTitleBarV2', {}, true);
bridge('showTitleBarV2', true);
bridge('showTitleBarV2', 123, true); // 报错    Argument of type '123' is not assignable to parameter of type 'ParamsInterface'.

在泛型约束中使用类型参数

function getProperty<T, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}

let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, 'm'); // 报错:TS2345: Argument of type '"m"' is not assignable to parameter of type '"a" | "b" | "c" | "d"'.