TypeScript-接口的使用

前言

众所周知,在传统的JavaScript中是没有接口的概念的,所谓的接口,其实就是描述集合属性的类型的一个特殊的虚拟结构。这也是开发一个大型项目所必须的语言特性,像Java、C#这样强类型语言,接口已经使用得非常广泛。于是,在TypeScrip中也引入了接口的概念。

一、接口的基本使用

基与我们前面介绍的对象的类型的声明,可以定义一个函数的参数是包含特定属性的对象:

function doSomeThing(params: {name: string}):void {
  console.log(params); 
}

console.log(doSomeThing({name: '马松松'}));

// { name: '马松松' }

我们也可以使用接口的方式实现上面的例子:

interface person {
  name: string
}
function doSomeThing(params: person):void {
  console.log(params); 
}

console.log(doSomeThing({name: '马松松'}));

// { name: '马松松' }

两者是等效的,使用接口的好处是可以将参数类型的配置抽离到一个单独的文件,这样使得项目更容易维护。

二、接口中使用可选参数

为了增强接口的灵活性和延展性,TypeScript允许定义为接口类型的变量可以选择性匹配。

interface SquireParams {
  width?: number,
  height?: number
}

function squireResult(params: SquireParams):any {
  let result: any;
  if (params.width) {
    result = params.width * params.width;
  }
  if (params.height) {
    result = params.height * params.height;
  }
  if (params.width && params.height) {
    result = params.width * params.height;
  }

  return result;
}

console.log(squireResult({height: 5}));
// 25

console.log(squireResult({width: 5}));
// 25

console.log(squireResult({width: 5,height: 5}));
// 25

当然,也可以和必选参数结合使用:

interface SquireParams {
  width?: number,
  height?: number,
  label: string
}

function squireResult(params: SquireParams):any {
  let result: any;
  if (params.width) {
    result = params.label +  params.width * params.width;
  }
  if (params.height) {
    result = params.label + params.height * params.height;
  }
  if (params.width && params.height) {
    result = params.label + params.width * params.height;
  }

  return result;
}

console.log(squireResult({label: '计算结果为:', height: 5}));
// 计算结果为:25

三、接口中使用 只读属性

同时,在JavaScript中,没有关键字标识只读属性。我们可以通过Object.defineProperty属性设置拦截,在TypeScript中明确提出了只读属性的关键字。

可以这样使用:

interface readonlyType {
  readonly x: number,
  readonly y: number
}

let readonlyObj: readonlyType = {x: 10, y: 10}
readonlyObj.x = 13;

//Cannot assign to 'x' because it is a read-only property 

只允许初始化的时候,给xy分配number的值。

对于数组,TypeScript也提供了ReadonlyArray这样的泛型只读数组,删除了该命名数组的操作数组的所有方法。

const arr: ReadonlyArray = [1,2,3];

当你想往该数组推入一个数字时,会引发错误:

arr.push()
// Property 'push' does not exist on type 'readonly number[]'

⚠️对于const和readonly的使用的场景:

TypeScript的官方推荐是:变量使用const,而属性使用readonly。

四、Excess Property Checks

这个是解决原生的JavaScript的行为和TypeScript行为不一致的方案,思考这样一个例子:

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

function createSquare(config: SquareConfig): { color: string; area: number } {
  return { color: config.color || "red", area: config.width ? config.width * config.width : 20 };
}

我们定义了一个SquareConfig接口,然后作为函数的入参类型,然后我们这样使用这个函数:

let mySquare = createSquare({ colour: "red", width: 100 });

这里TypeScript会给出错误提示类型不匹配,但是按照我们之前说的可选参数的的例子,这里的color并不是必须的,因为这里故意将color拼成了colour,TypeScript对以字面量方式定义对象的方式进行了特殊的类型检查处理,而在原生的JavaScript中是静默忽略的,为了避免出现这种情况,下面是几种更好的规避这种错误的方式:

1.使用as 强制推断类型

let mySquare = createSquare({colour: "red", width: 100} as SquareConfig);

2.不使用字面量的方式

let paramsSquare = {colour: "red", width: 100};
let mySecondSquare = createSquare(paramsSquare);

3.加一个额外的动态属性

interface SquareConfig {
  color ?: string,
  width ?: number,
  [propName: string]: any;
}

let myThirdSquare = createSquare({colour: "red", width: 100});

当你想用传字面量的方式传入参数,为了规避不必要的错误,使用上面的几种方式就行。

五、在接口中定义 函数的参数类型和返回值类型

1.基本使用:

首先定义一个函数的接口,我们定义了参数的类型和返回值的类型

interface baseFunc {
  (firstName: string, lastName: string): string
}

然后这样使用这个接口:

let myFunc: baseFunc  = function (firstName: string, lastName: string) {
  return firstName + lastName;
}

2.函数的入参不需要同名

let mySecondFunc: baseFunc  = function (fName: string, lName: string) {
  return fName + lName;
}

3.当你指定了函数签名的类型 函数的入参和返回值也不需要指明类型,类型系统会自动根据传入的参数推断类型

let myThirdFunc: baseFunc  = function (fName, lName) {
  return fName + lName;
}

4.但是如果你没有指定类型 但是返回了和接口返回类型不一致 类型检查不会通过

let myLastFunc: baseFunc  = function (fName, lName) {
  let result =  fName + lName;

  return 11;
}

六、接口中 定义数组和对象的索引类型

1.基本使用:

interface objectType {
  [index: string]: string;
}

在对象中这样使用这个接口

let myObj: objectType = {name: '马松松', age: "18"}; 

可以看到,我们定义的索引是string,属性值也是string,所以这样定义是合理的。

但是如果将age的属性定义为number类型,就不符合我们接口的定义:

let myObj: objectType = {name: '马松松', age: 18}; // 这样是不符合接口的定义的

在数组中需要这样使用定义接口,数组的索引都是number类型的:

interface arrayType {
  [index: number]: string;
}

然后,你可以这样使用这个接口:

let myArr: arrayType = ["马松松","18"];

2.注意字符串索引和直接指定类型的方式一起使用的时候,字符串索引类型的优先级更高,所以直接指明属性的类型 需要保持和字符串索引一样.

interface numberDictionary {
  [index: string]: number,
  length: number,
  // name: string // 这里使用string会报错,以为你字符串索引返回的类型是number
  name: number, // 这样是可以的
}

3.那你确实想定义不同类型的属性 可以这样做

interface secondNumberDictionary {
  [index: string]: number | string,
  length: number,
  name: string // 这样是可以的
}

4.也可以结合 readonly 定义只读属性

interface thirdNumberDistionary {
  readonly [index: string]: string
}

// 此时当你想设置thirdNumberDistionary的属性的时候就会报错
let myThirdNumberDictionary: thirdNumberDistionary = {name: '马松松'};
// myThirdNumberDictionary.name = "宋志露"; // 不可设置

七、类和接口的关系

其他语言中使用接口做频繁的操作的就是,用类实现一个接口,从而使得类和接口缔结某种强制的联系。

1.基本使用:

我们首先定义一个日期接口:

interface BaseClock {
  currentTime: string
}

使用implements关键词缔结类和接口的契约关系:

class MyClock implements BaseClock {
  currentTime: ""
  constructor(h: number, m: number) {

  }
}

缔结的契约关系为:MyClock类中必须有类型为stringcurrentTime变量。

2.也可以缔结类中的方法的契约

先定义接口:

interface SecondBaseClock {
  getCurrentTime(t: string): void
}

使用implements缔结契约:

class MySecondClock implements SecondBaseClock {
  getCurrentTime(t: string) {
    this.currentTime = t;
  }
}

缔结的契约关系为:MySecondClock类中需要有一个getCurrentTime方法,且需要一个类型为string的入参,没有返回值。

3.在缔结类和接口的契约关系时 注意new关键词

当使用new关键词实例化类时,TypeScript类型检查器不会检查静态类的构造器方法是否满足缔约,而是在你使用new关键词的时候判断是否满足。

比如我们定义一个构造函数的的接口:

interface C {
  new (hour: number, min: number)
}

然后使用implements缔结契约:

class Clock implements C {
  constructor(h: number, m: number) {}
}

我们会得到报错信息:

Class 'Clock' incorrectly implements interface 'C'.Type 'Clock' provides no match for the signature 'new (hour: number, min: number): any'

我们缔结契约的类实际上是满足了构造函数的接口的,但是由于TypeScript类型检查不会直接检查类中构造函数是否满足契约,所以这里会报错。

所以正确的使用方式是将缔结契约的类赋值给别的变量,这样类型检查系统就会进行类型检查:

interface ClockInterface {
  tick(): void
}
const Clock: ClockConstructor = class Clock implements ClockInterface {
  constructor(h: number, m: number) {}
  tick() {

  }
}

这里注意这样的区别就好了。

八、接口中 使用继承

1.基本使用

我们首先定义一个Square接口:

interface Square {
  width: number,
  height: number
}

然后这样使用:

let square = {} as Square;
square.width = 100;
square.height = 100;

为了使接口可以更灵活的构建更复杂的数据结构,这里使用到了extends关键字:

interface baseSquare {
  width: number,
}

interface Square extends baseSquare {
  height: number
}
let square = {} as Square;
square.width = 100;
square.height = 100;

2.一个接口可以继承多个接口

interface baseFirstSquare {
  width: number,
}
interface baseSecondSquare {
  width: number,
}

然后我们可以同时继承这样两个接口:

class MySquare implements baseFirstSarare,baseSecondSquare {
  color: string
}

九、接口中使用 混合类型

基于JavaScript语言的丰富性和灵活性,TypeScript允许使用混合类型

比如定义一个定时器接口:

interface Counter {
  (start: number): string;
  interval: number;
  reset(): void;
}

然后你这样使用:

function getCounter(): Counter {
  let counter = function (start: number) {} as Counter;
  counter.interval = 123;
  counter.reset = function () {};
  return counter;
}

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

这里使用as推断了类型,就获取了一个对象,这里个人有点不理解。

九、接口继承

1.当继承的类是public时,可以直接实现这个接口

class Control {
  public state: any
}

interface SelectableControl extends Control {
  select(): void
}

这样使用:

let select: SelectableControl = {
  state: 22,
  select() {}
}

2.当继承的类private或者protected时 继承的接口只能通过被继承类子类去实现,不能直接实现

class SecondControl {
  private state: any
}

interface SecondSelectableControl extends SecondControl {
  select(): void
}

只能是被继承类的子类去实现该接口,因为只有被继承类的子类才能访问私有属性:

class MySecondSelectableControl extends SecondControl implements SecondSelectableControl {
  select() {

  }
}

然后你这样使用:

let s = new MySecondSelectableControl();

总结:接口的使用,其实也是引进了强类型语言的相关的概念,理解接口概念的同时,同时也能增强前端开发者对强类型语言和弱类型语言的特性的理解。

你可能感兴趣的:(TypeScript-接口的使用)