Typescript 学习之 - Interface

1,236 阅读9分钟

接口是什么

TS 中文文档解释

TypeScript的核心原则之一是对值所具有的结构进行类型检查。 它有时被称做“鸭式辨型法”或“结构性子类型化”。 在TypeScript里,接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约。

对于初看ts的我来说,感觉比较抽象,于是先抛开概念,整体过了一遍文档,后来浏览到 Learning TypeScript — Interfaces,看到这篇文章的定义和示例,突然有了新的理解。

以下为原文中部分翻译

Simply put, an interface is a way of describing the shape of an object. In TypeScript, we are only concerned with checking if the properties within an object have the types that are declared and not if it specifically came from the same object instance.

简单的说,接口就是对 对象形状 的描述。

TypeScript 中,我们只关心一个对象所拥有的属性是否是我们定义的类型,而不关心他们是否是同一个实例。

interface NameValue {
    name: string
}

let thisObj = { name: 'Bryan' };
let thatObj = { name: 'Bryan' };

function printName(name: NameValue) {
    console.log(name);
}

printName(thisObj); // 'Bryan'
printName(thatObj); // 'Bryan'

console.log(thisObj === thatObj) // false

To emphasize how TypeScript only checks the shape of objects, we have thisObj and thatObj with the same property, name. Despite being two different objects, both objects pass type checking when printName() takes each of them as arguments.

为了突出 Typescript 是如何只检查对象的形状,示例中创建了 拥有相同属性的对象 thisObjthatObj,尽管是不同的对象,当调用printName方法的时候都通过了类型检查。

读到此处,再来看看官网的定义的 鸭式辨型法(像鸭子一样走路并且嘎嘎叫的就叫鸭子) 是不是更好理解了。

结合 tutorialsteacher 的教程定义总结:

  • 接口是对象形状的描述
  • 接口定义了要遵循的类的语法
  • Typescript 不会将接口转为Javascript ,而是用接口进行类型检查。
  • 接口可以定义类型,也可以在类中实现他(在类类型中会体现类对接口的实现)

接口属性和类型

可选属性

语法:

属性名?: 类型描述;
eg: color?: string;

应用场景和作用:对可能存在的属性预定义,对于没有罗列的属性会得到一个错误类型提示

如下所示:

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

function createSquare(config: SquareConfig): { color: string; area: number } {
  let newSquare = { color: "white", area: 100 };
  if (config.clor) {
    // ⚠️⚠️⚠️ Error: Property 'clor' does not exist on type 'SquareConfig'
    newSquare.color = config.clor;
  }
  if (config.width) {
    newSquare.area = config.width * config.width;
  }
  return newSquare;
}

let mySquare = createSquare({ color: "black" });

只读属性

语法:

只读的对象属性

readonly 属性: 类型描述;
eg: readonly x: number;

只读的数组属性

ReadonlyArray<T>
eg: let arr:ReadonlyArray<number> = [1,2,3,4,6]

应用场景:一旦创建不能被修改的属性

interface Point {
    readonly x: number;
    readonly y: number;
}
let p1: Point = { x: 10, y: 20 };
p1.x = 5; //  ⚠️⚠️⚠️ Error! Cannot assign to 'x' because it is a constant or a read-only property.

额外的属性检查

为了能够允许 interface 接受没有存在的属性,可以这样写:

  • 推荐: 字符串索引签名 [propName:string]:any
interface SquareConfig {
	color?: string;
	width?: string;
	[propName:string]: any;
}
const square:SquareConfig = {
	color: 'red',
   	opacity: 0.5
} // OK

由于额外属性的类型为any,所以 Typescript 的校验是通过的。

  • 将有额外属性的对象赋值给 没有进行额外属性描述的对象
interface SquareConfig {
    color?: string;
    width: number;

}
let squareOptions= {  width: 100,color: 'red', opacity: 0.5 };
let squareOptions2:SquareConfig = squareOptions

之所以这样也ok,是因为通过赋值的检测会比对象字面量的形式较松散,只要对象中,对于接口描述的属性类型正确即可。其他额外的属性不会进行校验。

函数类型

接口除了可以描述 Object / Array类型,也可以描述 Function 类型

相当于有字符串索引签名,对应的也有 call signature, 比较像 function declaration(需要描述参数类型和返回类型)

interface PrintNums = {
    (num1: number, num2: number): void; // void means it does not return anything
}

let logNums: PrintNums;

logNums = (num1, num2) => {
    console.log(num1,num2);
}

函数logNums 可以不用再指定类型了( logNums = (num1:number, num2:number):void => {}) ,TypeScript的类型系统会推断出参数类型,即通过 logNums: printNums 找到了 printNums 对应的类型描述。

可索引的类型

可索引的类型,是指可以通过索引得到的类型,比如 a[0]a['hi'],可用于描述数组、对象成员的属性。

可索引的类型结构如下图所示,具有一个索引签名,它描述了对象索引的类型,还有相应的索引返回值类型

描述数组成员

interface numberArray {
    [propname:number]:number
}
const nums:numberArray = [1,3,5]

描述对象成员

interface Obj {
    [propname: number]: number,
    name:string,
}
const nums: Obj = {
    0: 1,
    name: 'lisa'
}

TypeScript支持两种索引签名:字符串和数字。不过数字类型的返回值必须是字符串类型返回值的子类。

class Animal {
    name: string;
}
class Dog extends Animal {
   age:number
}

interface Ok {
    [x: string]: Dog;
    [x: number]: Animal; // Error : Numeric index type 'Animal' is not assignable to string index type 'Dog'.
}

由于在使用索引时,javascript 会将数字索引转为字符串,所以Dog 需要为 Animal 的子类。 上述示例正好相反。

这个例子改写后的正确方式为:

class Animal {
    name: string;
}
class Dog extends Animal {
   age:number
}

interface Ok {
    [x: string]: Animal; // 字符串索引签名
    [x: number]: Dog; // 数字索引签名
}
const animal: Animal = new Animal()
animal.name='bird'
const dog: Dog = new Dog()
dog.age = 1;
dog.name = 'pop'; 

const animals: Ok = {
    'cat': animal,
    0: dog,
}

需要注意的是,但设定了索引签名,接口中的其他同类型的索引签名的返回值,必须满足该索引签名的返回值

interface AnimalDefinition {
    [index: string]: string;
    name: string; // Ok as return type is "string"
    legs: number; // Error
}

类类型

实现接口

与 Java 和 C#之类的语言相似,TypeScript中的接口可以用 Class 实现。 实现接口的类需要严格符合接口的结构。

interface IEmployee {
    empCode: number;
    name: string;
    getSalary:(number)=>number;
}

class Employee implements IEmployee { 
    empCode: number;
    name: string;

    constructor(code: number, name: string) { 
                this.empCode = code;
                this.name = name;
    }

    getSalary(empCode:number):number { 
        return 20000;
    }
}

let emp = new Employee(1, "Steve");

上述示例中,类 Employee 通过关键词 implement 实现了接口 IEmployee 。实现类应严格定义具有相同名称和数据类型的属性和函数。

如果实现类没有严格遵守接口的类型,Typescript 将会报错。

当然实现类可以定义额外的属性和方法,但是至少要定义接口中的属性和方法。

类静态部分与实例部分的区别

我觉得文档中关于这部分的描述算是比较模糊的,为何官网中的示例会报错

interface ClockConstructor {
    new (hour: number, minute: number);
}
// Error:
//  Class 'Clock' incorrectly implements interface 'ClockConstructor'.
//  Type 'Clock' provides no match for the signature 'new (hour: number, minute: number): any'.
  
class Clock implements ClockConstructor {
    currentTime: Date;
    constructor(h: number, m: number) { }
}

参考 stackoverflow 的一个提问 Difference between the static and instance sides of classes 可能会稍微好理解一些。

The interface declares the method/members that the instances have, and not what the implementing class has.

即:接口只声明 实例 具有的方法/属性,而 constructor 不属于实例部分,因此按照文档的示例才会报错。

因此需要明确两个概念

  • 接口只声明 实例 具有的方法/属性。
  • constructor 存在于类的静态部分,所以不在检查的范围内。

如何实现 constructor 部分的检查?stackoverflow 的这篇文章也做了回答,参考 Array 的实现

interface Array<T> {
    length: number;
    toString(): string;
    toLocaleString(): string;
    push(...items: T[]): number;
    pop(): T | undefined;
    ...
    [n: number]: T;
}

interface ArrayConstructor {
    new (arrayLength?: number): any[];
    new <T>(arrayLength: number): T[];
    new <T>(...items: T[]): T[];
    (arrayLength?: number): any[];
    <T>(arrayLength: number): T[];
    <T>(...items: T[]): T[];
    isArray(arg: any): arg is Array<any>;
    readonly prototype: Array<any>;
}

也就是将构造函数部分和示例部分拆分定义,Array 是对实例部分的描述, ArrayConstructor 是对构造函数部分的描述。

以下是对官网示例的拆解

interface ClockConstructor { // ClockConstructor 构造函数的描述
    new (hour: number, minute: number): ClockInterface; 
}
interface ClockInterface  { // ClockInterface 实例的描述
    tick();
}

// createClock 作用:实现对 constructor 的检查
function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {
    return new ctor(hour, minute); // ctor 为传入的类,本示例中的 DigitalClock、AnalogClock
  //  DigitalClock 、AnalogClock 实现了接口 ClockInterface,故createClock 返回的类型为 ClockInterface
}

class DigitalClock implements ClockInterface {
    constructor(h: number, m: number) { }
    tick() {
        console.log("beep beep");
    }
}
class AnalogClock implements ClockInterface {
    constructor(h: number, m: number) { }
    tick() {
        console.log("tick tock");
    }
}

let digital = createClock(DigitalClock, 12, 17);
let analog = createClock(AnalogClock, 7, 32);

由于在 new 一个类的时候会触发 constructor ,因此创建 createClock 函数,通过 new ctor来实现对constructor的类型检查。由于类DigitalClockAnalogClock是对接口 ClockInterface 的实现,因此函数createClock 返回的类型为 ClockInterface

继承接口

类似于类的继承,接口也可以继承。

这样可以复制其他接口,从而实现接口的扩展。

一个接口也可以继承多个接口。

interface Shape { 
     stokeWidth: number;
}
interface PenColor { 
     color: string;
}
interface Circle extends Shape, PenColor {
    solid: boolean
}

const circle: Circle = {
    stokeWidth: 1,
    color: 'red',
    solid: false
}

混合类型

可能会有这样的场景,一个对象可以同时做为函数和对象使用,并带有额外的属性。 比如:

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}
Vue.version = '__VERSION__'

export default

对于此种场景的接口描述可以通过以下方式

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

function getCounter(): Counter {
    let counter = <Counter>function (start: number) { }; // 此处使用了断言
    counter.interval = 123;
    counter.reset = function () { };
    return counter;
}

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

Counter 接口混合了函数、函数属性的描述,其实官网的示例中加入了断言

let counter = <Counter>function (start: number) { };

如果移除断言,会遇到报错

报错信息提缺少属性 interval,但是加了断言就失去了对于函数的检查意义,示例中定义了接口的函数返回类型为字符串,但是断言后没有返回也没报错。

最后通过请教和查找,如果想既实现类型检查又能满足创建函数的同时创建函数属性,可以通过 Object.assign<T, U>(t: T, u: U) 来实现

interface Counter {
    (start: number): string;
    interval: number;
    reset(): void;
}
let c: Counter = Object.assign(
(start:number)=> { return start+'' },
    {
        interval: 10,
        reset: ():void => { }
    }
)
c(10);
c.reset();
c.interval = 5.0;

魔法在于 Object.assign<T, U>(t: T, u: U) 能够返回 T & U

接口继承类

// working

参考资料