TypeScript(五)接口(interface)与类型别名(type)

1,525 阅读10分钟

接口(Interface)

接口的定义

和java语言相同,TypeScript中定义接口也是使用 interface 关键字来定义:

interface IQuery {
    pagenumber;
}

接口的好处

JavaScript中定义一个函数,用来获取一个用户的姓名和年龄的字符串:

const getUserInfo = function(user) {
    return `name: ${user.name}, age: ${user.age}`
}

函数调用:

getUserInfo({name"koala"age18})

这对于我们之前写JavaScript的时候,再正常不过了,但是如果这个getUserInfo在多人开发过程中, 如果它是个公共函数,多个开发者都会调用,如果不是每个人点进来看函数对应注释,可能会出现以下 问题:

// 错误的调用
getUserInfo() // Uncaught TypeError: Cannot read property 'name' of undefined
console.log(getUserInfo({name"kaola"})) // name: kaola, age: undefined
getUserInfo({name"kaola"height1.66}) // name: kaola, age: undefined

JS是弱类型的语言,所以并不会对我们传入的代码进行任何的检测。

TypeScript中的 interface 可以解决这个问题

const getUserInfo = (user: {namestringagenumber}): string => {
    return `name ${user.name} age: ${user.age}`;
}

正确的调用时如下的方式:

getUserInfo({name"kaola"age18});

如果调用者出现了错误的调用,那么TypeScript会直接给出错误的提示信息:

// 错误的调用
getUserInfo(); // 错误信息: An argument for 'user' was not provided.
getUserInfo({name"coderwhy"}); // 错误信息: Property 'age' is missing in
type '{ name: string; }'
getUserInfo({name"coderwhy"height1.88}); // 错误信息:类型不匹配

这时候你会发现这段代码还是有点长,代码不便阅读,这时候就体现了interface的必要性。

使用 interface 对 user 的类型进行重构

我们先定义一个IUser接口:

// 先定义一个接口
interface IUser {
   namestring;
   agenumber;
}

接下来我们看一下函数如何来写:

const getUserInfo = (userIUser): string => {
   return `name: ${user.name}, age: ${user.age}`;
}
​
// 正确的调用
getUserInfo({name"koala"age18})

// 错误的调用和之前一样,报错信息也相同不再说明。

接口中函数的定义再次改造

定义两个接口:

type IUserInfoFunc = (user: IUser) => string;
​
interface IUser {
    namestring;
    agenumber;
}

接着二摸去定义函数和调用函数即可:

const getUserInfoIUserInfoFunc = (user) => {
    return `name: ${user.name}, age: ${user.age}`;
}

// 正确的调用

getUserInfo({name"koala"age18})

// 错误的调用

getUserInfo();

接口中定义方法

定义接口时候不仅仅可以有属性,也可以有方法,如下:

interface IQuery {
    pagenumber;
    findOne(): void;
    findAll(): void;
}

如果我们有一个对象是该接口类型,那么必须包含对应的属性和方法(无可选属性情况):

const qIQuery = {
    page1,
    findOne() {
        console.log("findOne");
    },
    findAll() {
        console.log("findAll");
    }
}

接口中定义属性

普通属性

上面的page就是普通属性,如果有一个对象是该接口类型,那么必须包含对应的普通属性。就不具体说了

可选属性

默认情况下一个变量(对象)是对应的接口类型,那么这个变量(对象)必须实现接口中所有的属性和方法。

但是,开发中为了让接口更加灵活,某些属性我们可能希望设计成可选的(想实现跨越实现,不想实现 也没有关系),这个时候就可以使用可选属性

interface IQuery {
    pagenumber;
    findOne(): void;
    findAll(): void;
    isOneline?: string | number// 是否出售中商品
    delete?(): void
}

上面的代码中,增加了isOnline属性和delete方法,这两个都是可选的;

注意:可选属性如果没有赋值,那么获取到的值是 undefined ;对于可选方法,必须先进行判断,再调用,否则会报错;

const pIQuery = {
    page1,
    findOne() {
        console.log("findOne");
    },
    findAll() {
        console.log("findAll");
    }
}
​
console.log(p.isOneline); // undefined
p.delete(); // 不能调用可能是“未定义”的对象

正确的调用方式如下:

if(p.delete) {
    p.delete();
}

大家可能会问既然是可选属性,可有可无的,那为什么还要定义呢?对比起完全不定义,定义可选的属性主要是:为了让接口更加的灵活,某些属性我们可能希望设计成可选,并且如果存在属性,能约束类型,而这也是十分关键的。

只读属性

默认情况下,接口中丁一一的属性可读可写:但是有一个关键字readonly,定义的属性值,不可以进行修改,强制修改后报错。

interface IQuery {
    readonly pagenumber;
    findOne(): void;
}

page属性加了readonly关键字,再给它赋值会报错。

const qIQuery = {
    page1,
    findOne() {
        console.log("findOne");
    }
};
q.page = 10// Cannot assign to 'page' because it is a read-only property

任意属性

interface IQuery {
    pagenumber;
    delete?(): void;
    [propName: string]: any;
}

const test: IQuery {
    page: 10,
    size: 20
}

propName可为任意值

函数类型接口

Interface还可以用来规范函数的形状。Interface里面需要列出参数列表返回值类型的函数定义。写法如下:

  • 定义了一个函数接口
  • 接口接收三个参数并且不返回任何值
  • 使用函数表达式来定义这种形状的函数
interface Func {
    // 定义与函数接收两个必选参数都是 number 类型,以及一个可选的字符串参数desc,这个函数不返回任何值
    (xnumberynumber, desc?: string): void
}
const sumFunc = function(x, y, desc = '') {
    // const sum: Func = function (x: number, y: number, desc: string): void
    // ts 类型系统默认推论可以不必书写上述类型定义
    console.log(desc, x + y)
}
sum(3222)

注意:不过上面的接口中只有一个函数,TypeScript会给我们一个建议,可以使用 type 来定义一个函数的类型:

type Func = (x: number, y: number, desc?: string) => void;

接口实现

接口除了定义某种类型规范,也可以和其它编程语言一样,让一个类去实现某个接口,那么这个类就必须明确去拥有这个接口中的属性和实现其方法:

下面的代码中会有关于修饰符的警告,暂时忽略,后面详细讲解 // 定义一个实体接口

interface Entity {
    titlestring;
    log(): void
}

// 实现这样一个接口

class Post implements Entity {
    titlestring;
   
    constructor(title: string) {
        this.title = title;
    }
   
    log(): void {
        console.log(this.title);
    }
}

有些小伙伴的疑问?我定义了一个接口,但是我在继承这个接口的类中还要写接口的实现方法,那我不 如直接就在这个类中写实现方法岂不是更便捷,还省去了定义接口?

先记住两个字,规范

这个规可以达到你一看这个名字,就知道他是用来干什么的,并且可拓展,可维护。

  • 在代码设计中,接口是一种规范;接口通常用于定义某种规范,类似于你必须遵守的协议,
  • 站在程序角度上说接口只规定了类里必须提供的属性和方法,从而分离了规范和实现,增强了系统的可拓展性和可维护性;

接口的继承

和类一样,接口也能继承其他的接口。这相当于复制接口的所有成员。接口也是用关键字 extends 来继承。

interface Shape { // 定义接口Shape
    colorstring;
}
​
interface Square extends Shape { // 继承接口Shape
    sideLengthnumber;
}

一个interface可以同时继承多个interface,实现多个接口成员的合并。用逗号隔开要继承的接口。

interface Shape {
    colorstring;
}
​
interface PenStroke {
    penWidthnumber;
}
​
interface Square extends ShapePenStroke {
    sideLengthnumber;
}

需要注意的是,尽管支持继承多个接口,但是如果继承的接口中,定义的同名属性的类型不同的话是不能编译通过的。如下代码:

interface Shape {
    colorstring;
    testnumber;
}
​
interface PenStroke extends Shape{
    penWidthnumber;
    teststring;
}

另外关于继承还有一点,如果现有一个类实现了Square接口,那么不仅仅需要实现Square的方法,也需要实现Square继承自接口中的方法,实现接口使用implements关键字

type和interface的区别

interface和type两个关键字因为其功能比较接近,常常引起疑问:应该再声明时候用 type,什么时候用 interface?

interface的特点如下:

  • 同名interface自动聚合,也可以和已有的同名class聚合,适合做polyfill
  • 自身只能表示object/class/function的类型

建议库的开发者所提供的公共 api 应该尽量用 interface/class,方便使用者自行扩展。

举例:

/**
* Cloud Studio使用的monaco版本较老0.14.3,和官方文档相比缺失部分功能
* 另外vscode有一些特有的功能,必须适配
* 故在这里手动实现作为补充
*/
declare module monaco {
    interface Position {
        delta(deltaLineNumber?: number, deltaColumn?: number): Position
    }
}
​
// monaco 0.15.5
monaco.Position.prototype.delta = function(this: monaco.Position, deltaLineNumber = 0, deltaColumn = 0) {
    return new monaco.Position(this.lineNumber + deltaLineNumber, this.column + deltaColumn);
}

与interface相比,type的特点如下:

  • 表达功能更强大,不局限于 object/class/function
  • 要扩展已有type需要创建新type,不可以重名
  • 支持更复杂的类型操作
type Tuple = [numberstring];
const aTuple = [2'sir'];
type Size = 'small' | 'default' | 'big' | number;
const bSize = 24;

基本上所有用interface表达的类型都有其等价的type表达。但在实践的过程中,也发现了一种类型只能用interface表达,无法用type表达,那就是往函数上挂载属性。

interface FuncWithAttachment {
    (paramstring): boolean;
    somePropertynumber;
}
​
const testFuncFuncWithAttachment = ...;
const result = testFunc('mike'); // 有类型提醒
testFunc.someProperty = 3// 有类型提醒

type可以而interface不行

type可以声明基本类型别名,联合类型,元组等类型

// 基本类型别名
type Name = string// 联合类型
interface Dog {
   wong();
}
interface Cat {
   miao();
}
​
type Pet = Dog | Cat// 具体定义数组每个位置的类型
type PetList = [DogPet]

type语句中还可以使用typeof获取实例的类型进行赋值

// 当你想获取一个变量的类型时,使用typeof
let div = document.createElement('div');
type B = typeof div

type其他骚操作

type StringOrNumber = string | number;
type Text = string | { textstring };
type NameLookup = Dictionary<stringPerson>;
type Callback<T> = (data: T) => void;
type Pair<T> = [T, T];
type Coordinates = Pair<number>;
type Tree<T> = T | { leftTree<T>, rightTree<T> };

interface可以而type不行

interface能够声明合并

interface User {
    namestring
    agenumber
}
​
interface User {
    sexstring
}
​
/**
User 接口为 {
    name: string
    age: number
    sex: string
}
*/

另外关于type的更多内容,可以查看文档:TypeScript官方文档

接口 vs 类型别名

类型别名看起来和接口非常类似,区别之处在于:

  • 接口可以实现 extends 和 implements,类型别名不行。
  • 类型别名并不会创建新类型,是对原有类型的引用,而接口会定义一个新类型。
  • 接口只能用于定义对象类型,而类型别名的声明方式除了对象之外还可以定义交叉、联合、原始类型等。

类型别名是最初 TypeScript 做类型约束的主要形式,后来引入接口之后,TypeScript 推荐我们尽可能的使用接口来规范我们的代码。

类型别名在定义交叉类型、联合类型时特别好用,要注意类型别名与接口的区别。

接口的应用场景

在项目中究竟怎么用,开篇已经举了两个例子,在这里再简单写一点。在写查询参数检验的时候,或者 返回固定数据的时候,都会用到接口,看一段简单代码,已经看完了上面的文章,自己体会下吧。

import User from '../model/user';
import Good from '../model/good';
​
// 定义基本查询类型
// -- 查询列表时候使用的接口
interface Query {
    pagenumber;
    rowsnumber;
    disabledPage?: boolean// 是否禁用分页,true将会忽略`page`和`rows`参数
}
   
// 定义基本返回类型
type GoodResult<Entity> = {
    listEntity[];
    totalnumber;
    [propNamestring]: any;
}
​
// 商品
export interface GoodsQuery extends Query {
    isOnline?: string | number// 是否出售中的商品
    goodsNo?: string// 商品编号
    goodsName?: string// 商品名称
}
​
export type GoodResult = QueryResult<Good>;