重学TS --- Interface

727 阅读6分钟

这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战

TypeScript 的核心原则之一是对值所具有的_结构_进行类型检查

在 TypeScript 里,接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约

基本使用

接口主要是用来定义对象等复杂数据类型解构的

TS只会去关注值的外形。 只要传入的对象满足接口中提到的必要条件,那么它就是被允许的

// 定义接口 --- 接口的首字母推荐使用I,且首字母需要大写 --- 约定俗称,不强求
interface Iuser {
  firstname: string,
  lastname: string
}

// 不使用接口
// function getFullName({ firstname, lastname }: { firstname: string, lastname: string }) {
//   return `${firstname} - ${lastname}`
// }

// 使用接口
function getFullName({ firstname, lastname }: Iuser) {
  return `${firstname} - ${lastname}`
}

getFullName({
  firstname: 'Klaus',
  lastname: 'Wang'
})

可选参数

接口里的属性不全都是必需的。 有些是只在某些条件下存在,或者根本不存在

带有可选属性的接口与普通的接口定义差不多,只是在可选属性名字定义的后面加一个?符号

可选属性的好处是可以对可能存在的属性进行预定义

interface Iuser {
  firstname: string,
  lastname?: string // 可选参数
}

function getFullName({ firstname, lastname = 'Wang' }: Iuser) {
  return `${firstname} - ${lastname}`
}


console.log(getFullName({
  firstname: 'Klaus'
}))

只读属性

一些对象属性只能在对象刚刚创建的时候修改其值。 你可以在属性名前用readonly来指定只读属性

interface Iuser {
  readonly firstname: string, // 只读属性
  lastname?: string
}

// 通过赋值一个对象字面量来构造一个Iuser
// 赋值后, firstname的值就不在被允许进行修改了
const user: Iuser = {
  firstname: 'Klaus'
}

// user.firstname = 'Alex' // error
interface Iarr {
  0: string,
  readonly 1: number
}

const arr: Iarr = ['Klaus', 23]

// arr[1] = 24 // error

定义函数结构

接口能够描述 JavaScript 中对象拥有的各种各样的外形。 除了描述带有属性的普通对象外,接口也可以描述函数类型。

interface IFoo {
  // 这里其实是一个调用签名 --- 它看上去就像一个只有参数列表和返回值类型的函数定义
  // 参数列表里的每个参数都需要名字和类型
  // 函数的参数名不需要与接口里定义的名字相匹配
  // TS会对函数的参数会逐个进行检查,只要求对应位置上的参数类型是兼容的即可
  (num1: number, num2: number): number
}

// 在下边例子中,num1和num2的类型是可以省略的,因为TS会根据接口类型IFoo推导出来
const foo: IFoo = (num1: number, num2: number) => num1 + num2

索引签名

所谓索引签名其实就是定义索引的类型,比如对象中key的类型或者数组中索引的类型

具有索引签名的类型就被称之为可索引类型

interface IFoo {
  // 如果一个对象的类型是IFoo
  // 那么这个对象中所有的key必须是字符串类型
  // 值必须是数值类型
  [key: string]: number
}

const foo: IFoo = {
  // name: 'Klaus' ---> error
  age: 23
}

如果存在具体的属性,那么索引签名的值的类型必须兼容具体key的类型,不可以存在冲突

// 类型“string”的属性“name”不能赋给“string”索引类型“number
interface IFoo {
  name: string, // ---> error
  age: number,
  [key: string]: number
}

可以进行如下修改

interface IFoo {
  name: string,
  age: number,
  [key: string]: number | string
}

Typescript 支持两种索引签名:字符串和数字。 可以同时使用两种类型的索引

但是数字索引的返回值必须是字符串索引返回值类型的子类型。 这是因为当使用number来索引时,JavaScript 会将它转换成string然后再去索引对象

class Animal {
  name: string;
}
class Dog extends Animal {
  breed: string;
}

// 错误:字符串类型的索引的类型Dog不是数字类型索引的类型Animal的子类型
interface NotOkay {
  [x: number]: Animal;
  [y: string]: Dog;
}

接口继承

接口也可以相互继承。 这让我们能够从一个接口里复制成员到另一个接口里,可以更灵活地将接口分割到可重用的模块里

interface IAnimal {
  name: string
  age: number
}

interface IPerson extends IAnimal {
  address: string
}

const person: IPerson = {
  name: 'Klaus',
  age: 23,
  address: 'shanghai'  
}

一个接口可以继承多个接口,创建出多个接口的合成接口

interface IName {
  name: string
}

interface IAge {
  age: number
}

interface IPerson extends IName, IAge {
  address: string
}

const person: IPerson = {
  name: 'klaus',
  age: 23,
  address: 'shanghai'
}

接口和类

与 C# 或 Java 里接口的基本作用一样,TypeScript 也能够用它来明确的强制一个类去符合某种契约

interface IPerson {
  name: string
  printName(name: string): void
}

class Person implements IPerson {
  name = 'Klaus'

  printName(name: string) {
    console.log(name)
  }
}

混合类型接口

在JS中,函数是一种特殊的对象,也就是说,函数即可以执行,也可以添加属性。同样,这在TS中也是支持的

interface ICounter {
  count: number,
  (): void
}

function getCounter(): ICounter {
  // increment即是一个函数,其本身也是一个对象,其上边也是有count属性存在的
  const increment = () => increment.count++
  increment.count = 0
  return increment
}

const counter: ICounter = getCounter()

counter()
console.log(counter.count)

多余属性检查

我们传入的对象参数实际上会包含很多属性,我们希望编译器只会检查那些必需的属性是否存在,并且其类型是否匹配。

但是接口在进行检测的时候,如果属性少了或者多了都会报错,此时我们就需要绕过TS的多余属性检测

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

function createSquare(config: SquareConfig): { color: string; area: number } {
  // ...
}

// 这里的colour其实是一个错误的属性 --- 这是一个bug
// 但是对于SquareConfig接口中,color属性和width属性都是可选属性
// 所以此时如果编译器只会检查那些必需的属性是否存在,并且其类型是否匹配
// 那么这种bug就会被忽略
let mySquare = createSquare({ colour: "red", width: 100 });

在TypeScript中,对象字面量会被特殊对待而且会经过_额外属性检查_,当将它们赋值给变量或作为参数传递的时候。 如果一个对象字面量存在任何“目标类型”不包含的属性时(存在额外的多余属性),你会得到一个错误。

但是有的时候,我们知道并不是代码bug,可能就是传入的对象参数上包含了多余的属性,此时我们就需要绕开TS的多余属性检查

方式一: 类型断言

interface IUser {
  firstname: string
  lastname: string
}

function printName(user: IUser) {
  console.log(`${user.firstname} - ${user.lastname}`)
}

printName({
  firstname: 'Klaus',
  lastname: 'Wang',
  age: 23
} as IUser)

方式二: 索引签名

// IUser对应的数据上必须存在firstname和lastname
// 以及其它key为string的任何属性值
interface IUser {
  firstname: string
  lastname: string
  [key: string]: any
}

function printName(user: IUser) {
  console.log(`${user.firstname} - ${user.lastname}`)
}

printName({
  firstname: 'Klaus',
  lastname: 'Wang',
  age: 23
})

方式三: 属性擦除

interface IUser {
  firstname: string
  lastname: string
}

function printName(user: IUser) {
  console.log(`${user.firstname} - ${user.lastname}`)
}


const user = {
  firstname: 'Klaus',
  lastname: 'Wang',
  age: 23
}

// 多余类型检测本质是针对于对象字面量,并不针对于变量
// 此时只要user上存在IUser上必须存在的firstname和lastname属性即可
// 多余的属性会被忽略
printName(user)