Vue & TypeScript 初体验 - TypeScript中的Interface

7,467 阅读8分钟

前文回顾

在这两篇中, 主要介绍了在vue 2.x版本中使用TypeScript面向对象编程时, 一些在编写Vue组件时语法上的变化. 以及使用Vuex时一些变化.

本文主要介绍下, 利用TypeScript语言的特性, 如何有效利用Interface进行面向接口编程.

一般在实现一个系统的时候,通常是将定义与实现合为一体,不加分离的,我认为最为理想的系统设计规范应是所有的定义与实现分离,尽管这可能对系统中的某些情况有点麻烦。

1. 什么是接口?

在面向对象语言中,接口Interfaces是一个很重要的概念,它是对行为的抽象,而具体如何行动需要由类class去实现implements

TypeScript 中的接口是一个非常灵活的概念,除了可用于对类的一部分行为进行抽象以外,也常用于对「对象的形状(Shape)」进行描述。

引申思考: 面向对象/面向过程/面向接口编程

  • 面向过程是指,我们考虑问题时,以一个具体的流程(事务过程)为单位,考虑它的实现
  • 面向对象是指,我们考虑问题时,以对象为单位,考虑它的属性及方法
  • 面向接口编程,原意是指面向抽象协议编程,实现者在实现时要严格按协议来办。

2. 什么要使用接口?

举例来说明一下.

假设一个场景, 我们需要根据userInfo对象, 获取用户的userId, 代码可能如下:

// utils/env.js
function getUserId(userInfo) {
    return userInfo.account.id
}

在JavaScript诸如此类的函数可能会很多, 有可能会引发以下问题:

  • 传入的实参数据结构不对, 要能会得到''空值.
  • 在迭代开发中, 有人误修改了形参的数据结构, 导致取不到期望的值, 原因排查困难(公用函数会在多处使用)

JavaScript是弱类型语言, 并不会对传入的参数做过多的检测, 需要我们自己添加相关判断. 以上示例代码可考虑修改为:

// utils/env.js
function getUserId(userInfo) {
    if(userInfo && Object.prototype.call(userInfo.account).slice(8, -1) === 'Object') {
        return userInfo.account.id
    }
    window.console.warn("Can't get user accout id, pls check!")
    return ''
}

相信工程当中很多核心的业务数据, 是需要强校验的.

但如果使用TypeScript中的Interface, 就简单多了.

使用TypeScript Interface实现

// utils/env.ts
interface Account {
  id: string;
  name: string;
}

interface UserInfo {
  account: Account;
  loginDate: string;
}

export function getUserId(userInfo:UserInfo):string {
  return UserInfo.account.id
}

3. TypeScript Interface

1). Interface 基础

TypeScript中定义接口使用interface关键字来定义.

如下简单示例:

// 定义接口
interface Account {
  id: string;
  name: string;
}
export default interface Person{
  id: number, // 必选属性
  name: string, // 必选属性
  age: number, // 必选属性
  job?: string, // 可选属性,表示不是必须的参数,
  readonly salary: number, // 只读属性, 表示是只读的属性,但是在初始化之后不能重新赋值,否则会报错
  [ propName : string ] : any, // 任意属性, 所有属性的属性名必须为string类型, 值可以是 任意类型
  getUserId(account:Accout): string; // 定义方法
}
// 定义一个变量,它的类型为接口Person,这样即可约束接口的内容了.
let person: Person = {
  name: 'james',
  age: 30,
  job: 'IT dog',
  id: 9527,
  salary: 1000000000,
  // 注意, hello, demo是显式定义的属性, 适用于[propName:string]: any规则, 因此值类型为: any
  hello: 'world',
  demo: [1, 3, 'world'],
  getUserId(account:Account):string{return account.id}
}

function printMan (person:Person) {
  window.console.log(`My name is: ${person.name}, my job is: ${person.job}`)
}

此例是一个简单的接口实例, 利用接口来约束传入变量的内容. 需要注意的是, 在赋值时, 变量的shape必须与接口shape保持一致.

接口类型的几类属性

  • 必选属性, ":" 带冒号的属性是必须的,不可少
  • 可选属性, "?" 带问号的属性, 表示是可选的
  • 只读属性, "readonly", 有时候我们希望对象中的一些字段只能在创建的时候被赋值,那么可以用 readonly 定义只读属性. ,只读的约束存在于第一次给对象赋值的时候,而不是第一次给只读属性赋值的时候:
  • 任意属性 [ propName : 类型 ] : any, 表示定义了任意属性取string 类型的值 需要注意的是,一旦定义了任意属性,那么确定必选和可选属性都必须是它的子属性

2). Interface 进阶

函数类型接口

Interface 可以用来规范函数的入参和出参。
例如:

interface Account{
    id: string;
    firstName: string;
    lastName: string;
    getUserFullName(firstName: string, lastName: string): string
}

以上接口中定义了一个getUserName函数, 用于获取用户的全名.

接口的实现

// 定义接口
interface Account{
  id: string;
  firstName: string;
  lastName: string;
  getUserFullName(firstName: string, lastName: string): string
}
// 实现接口
export default class Person implements Account {
  id: string;
  firstName: string;
  lastName: string;

  constructor(id: string, firstName: string, lastName: string) {
    this.id = id;
    this.firstName = firstName;
    this.lastName = lastName
  }
  getUserFullName (firstName: string, lastName: string) : string {
    return firstName + " " + lastName
  }
}

初学者可能会有疑问, 为何Interface需要预先定义, 而后又去实现它, 为何不可以直接实现呢?

其实主要有两方面的考虑:

  • 规范, 接口其实是一种规范, 通常用来:
    • 定义规范(或数据结构), 必须遵守该规范
  • 程序设计, 接口中定义了类必须实现的方法和属性, 以达到规范和具体实现分离, 增强系统的可拓展性和可维护性.

接口继承

单接口继承
interface Account{
  id: string;
  firstName: string;
  lastName: string;
  getUserFullName(firstName: string, lastName: string): string
}

// 接口继承使用`extends`
interface UserInfo extends Account {
  nickName: string;
}
多接口继承

一个接口可以同时继承多个interface, 实现多个接口成员的合并. 例如:

interface Email {
  domain: string;
  address: string;
}
interface Account{
  id: string;
  firstName: string;
  lastName: string;
  getUserFullName(firstName: string, lastName: string): string
}

// 多接口继承
interface UserInfo extends Account, Email {
  nickName: string;
}

需要注意的是, 在继承多个接口时

  • 定义的同名属性的类型不同的话,是不能编译通过的。
  • 在实现(implements)接口时, 需要实现所有继承接口中的属性和方法

例如, 若要实现接口UserInfo, 那么其继承的接口Account, Email中所有属性都要实现. 示例代码如下:

interface Email {
  domain: string;
  address: string;
}
interface Account{
  id: string;
  firstName: string;
  lastName: string;
  getUserFullName(firstName: string, lastName: string): string
}

interface UserInfo extends Account, Email {
  nickName: string;
}

class Person implements UserInfo{
  id: string;
  firstName: string;
  lastName: string;
  domain: string;
  address: string;
  nickName: string;
  constructor(id: string, firstName: string, lastName: string, domain: string, address: string): void{
    this.id = id;
    this.firstName = firstName;
    this.lastName = lastName;
    this.nickName = this.firstName;
    this.domain = domain;
    this.address = address;
  }

  getUserFullName (firstName: string, lastName: string) : string {
    return firstName + " - " + lastName
  }
}

3). 可索引类型的接口

interface StrArray {
  readonly [index: number]: string // 注意: index 只能为 number 类型或 string 类型
  length: number // 指定length属性
}

let strArr:StrArray = ['hello', 'james']
strArr[1] = 'demo' // 通过索引赋值是不被允许的, 因为设置了index为readonly属性

4). 类Interface

Interface 也可以用来定义一个类的形状。需要注意的是类 Interface 只会检查实例的属性,静态属性是需要额外定义一个 Interface;比如:

// Person.ts
// PersonConstructor => 用以检查静态属性或方法
interface PersonConstructor {
  new (name: string, age: number): void
  typename: string // 静态属性
  getType(): string // 静态方法
}

interface PersonInterface {
  log(msg: string): void
}

// 不可写成:  class Person implements PersonInterface, PersonInterface
const Person: PersonConstructor = class Person implements PersonInterface {
  name: string
  age: number
  static typename = 'Person type'
  static getType(): string {
    return this.typename
  }

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

  log(msg: string): void {
    window.console.log(msg)
  }
}

export default Person

需要注意的是: 静态属性和方法的检查, 与实例属性和方法的检查应使用不同的interface.

5). 继承类的Interface

Interface 不仅能够继承 Interface 还能够继承类,再创建子类的过程中满足接口的描述就会必然满足接口继承的类的描述

// ExtendsDemo.ts
class Animal {
  // 类的静态属性
  static clsName: string
}

interface Dog extends Animal {
  wangwang(): void
}

// 第一种方法, 实现Dog接口
class WangCai1 implements Dog {
  // Dog接口继承至Animal类, 因此规范了 clsName 属性
  static clsName: '旺财'
  // Dog接口有对wowo方法进行描述
  wangwang() {
    window.console.log('旺旺旺...')
  }
}

// 第二种方法, 继承Animal类, 实现Dog接口
class WangCai2 extends Animal implements Dog {
  static clsName: 'Wang Cai'
  wangwang() {
    window.console.log('旺旺旺...')
  }
}

4. type & interface的异同

1). 相同点

都可以用来描述一个函数或对象, 如:

// TypeAndInterface.ts
interface Person{
  name: string;
  age: number;
  getName():string;
}
type Person1 = {
  name: string;
  age: number;
  getName(): string;
}

都可以使用extends继承

// TypeAndInterface.ts
interface Person {
  name: string
  age: number
  getName(): string
}
type Person1 = {
  name: string
  age: number
  getName(): string
}

// type继承type声明的接口
type Person2 = {
  firstName: string
  lastName: string
}
type User = Person2 & { age: number }

// interface继承type声明的接口
interface User1 extends Person2 {
  age: number
}

// type继承interface声明的接口
type User2 = User1 & { getFullName(): void }

2). 不同点

type可以声明特定类型, 如:

  • 基本数据类型的别名
  • 联合类型
  • 元组等类型
  • 可以使用typeof获取实例的类型并进行赋值
// TypeAndInterface.ts

/**
 type可以声明基本类型别名, 联合类型, 元组等类型, 也可以通过使用typeof获取实例的类型进行赋值
 */
// 基本数据类型的别名
type Str = string

// 联合类型
type StrOrNumber = string | number
type Message = string | { text: string }
type Tree<T> = T | {left: Tree<T>, right: Tree<T>}

// 元组
// 具体指定UserArr的每个位置的数据类型
type UserArr = [string, number]
let demo: UserArr = ['hello', 30] // 只可以被依次赋值为: string, number, 否则会报错

type Arr<T> = [T, T];
type Coords = Arr<number>

// 使用typeof获取实例的类型, 并进行赋值
const img = window.document.createElement('img')
type ImgElement = typeof img

interface可以声明合并

// TypeAndInterface.ts

// interfact可以声明合并
interface Man {
  name: string
  age: number
}

interface Man {
  sex: string
}

/**
 * 等价于:
 interface Man{
   name: string;
   age: number;
   sex: string;
 }
 */

注意: 在项目中并不建议大家这么使用.

何时使用type/interface

在不确定使用type/interface时, 请优先考虑使用interface, 若interface无法满足需求时, 才考虑使用type.

相关链接