TypeScript(二)接口
TypeScript的核心就是对值所具有的结构进行类型检查。接口规定了行为和规范,起到限制和规范的作用。
接口使用 interface 关键字声明。一般首字母大写,根据ts命名规范,接口名加前缀 I 。
本文包括对象类型、函数类型、可索引的类型、混合类型的接口以及继承接口。
对象类型的接口
在函数调用时对传入的对象属性进行检查限制,可以通过以下方式:
function getFoodInfo({ name, color }: {name: string, color: string}): string {
  return `the ${name} color is ${color}!` 
}
通过接口来描述上例:
interface IFood {
  name: string;
  color: string;
}
function getFoodInfo({ name, color }: IFood): string {
  return `the ${name} color is ${color}!` 
}
getFoodInfo({
  name: 'cake',
  color: 'white'
})
类型检查器不会检查属性的顺序,只要相应的属性存在并且类型一致即可。
可选属性
接口中的属性并不都是必需的,有些属性在某些特定条件下存在。
带有可选属性的接口只是在可选属性名字定义的后面加一个 ? 符号。
interface AreaConfig {
  name: string;
  width?: number;
}
// width 属性为可选的
function getAreaInfo ({ name, width }: AreaConfig): { name: string, square: number } {
  const area = { square: 9, name }
  if (width) area.square = width * width
  return area
}
getAreaInfo({name: 'beijing'})
可选属性的好处:
- 可以对可能存在的属性进行预定义
 - 提前捕获引用了不存在的属性时抛出的错误
 
比如:
interface AreaConfig {
  name: string;
  width?: number;
}
// width 属性为可选的
function getAreaInfo ({ name, width }: AreaConfig): { name: string, square: number } {
  const area = { square: 9, name }
  // width 写成 widt
  if (widt) area.square = width * width //  Cannot find name 'widt'.
  return area
}
// 在调用时传入了width属性,在js中因为不存在widt属性所以导致width使用默认值undefined。但是在ts中通过类型检查器会提前报错避免潜在的问题。
getAreaInfo({ name: 'beijing', width: 12 })
只读属性
有些对象的属性在对象创建之后是不可修改的。可以在属性名前用 readonly 来指定只读属性:
对象属性
interface Food {
  readonly name: string;
  color: string;
}
const tomato: Food = {
  name: 'tomato',
  color: 'red'
}
tomato.name = 'potato' // Cannot assign to 'name' because it is a read-only property.
ReadonlyArray<T> 可以创建一个不可变得数组
ReadonlyArray<T> 类型与 Array<T> 相似,只是把所有可变方法去掉了,因此可以确保数组创建后再也不能被修改:
定义一个普通数组:
let arr: Array<number> = [1, 2, 3]
arr[1] = 4
console.log(arr) // [1, 4, 3]
创建一个不可改变的数组:
let array: ReadonlyArray<number> = [5, 6, 7, 8]
该数组的特性:
- 不可修改某一项:
 
array[1] = 55 
// Index signature in type 'readonly number[]' only permits reading.
- 不可删除、添加元素
 
array.push(7) 
array.splice(0, 1)
// Property 'splice' does not exist on type 'readonly number[]'.
- 不可修改数组length属性
 
array.length = 6
// Cannot assign to 'length' because it is a read-only property.
- 即便把整个ReadonlyArray赋值到一个普通数组也是不可以的。
 
arr = array
// The type 'readonly number[]' is 'readonly' and cannot be assigned to the mutable type 'number[]'.
但可以用类型断言重写:
arr = array as number[]
arr[1] = 555
console.log(array === arr) // true
// array 和 arr 指向同一个引用 ,并且可以通过arr去更改array的元素,但是仍然不可以直接通过array修改数组的元素
array[1] = 111 // Index signature in type 'readonly number[]' only permits reading.
readonly vs const 的区别
const 定义的变量不可重新赋值,但是其属性值是可改变的,而 readonly 定义的属性不可改变。
额外属性检查
先看一个例子:
interface Food {
  name: string;
  size: number;
  color?: string;
}
function getFood(info: Food): string {
  return `${info.name} size is ${info.size}, color is ${info.color}` 
}
getFood({ name: 'tomato', size: 10, colorX: 'red'})
// Argument of type '{ name: string; size: number; colorX: string; }' is not assignable to parameter of type 'Food'.
// Object literal may only specify known properties, but 'colorX' does not exist in type 'Food'.
在调用getFood时color拼写错误,因为color是可选的,有兼容性所以不会有问题。但是ts类型检查器检测出colorX这个额外属性从而会抛出错误。
绕开ts对额外属性的检查有三种方式:
1. 类型断言(最简便的方式)
getFood({ name: 'tomato', size: 10, colorX: 'red'} as Food)
2. 字符串索引签名(最佳方式)
interface Food {
  name: string;
  size: number;
  color?: string;
  [propName: string]: any;
}
3. 将对象赋值给另一个变量(类型兼容性)
const obj = { name: 'tomato', size: 10, colorX: 'red' }
getFood(obj)
函数类型
接口除了可以描述带有属性的普通对象外,还可以描述函数类型。
使用接口表示函数类型时,需要给接口定义一个调用签名,用来描述函数的参数和返回值的类型。如下:
interface FuncInterface {
  (num1: number, num2: number): number
}
let add: FuncInterface = (n1, n2) => {
  return n1 + n2
}
add(2, 5)
参数名不需要与接口里定义的参数名相同。此外由于TypeScript的类型系统会推断出参数类型,所以参数类型可以不指定。函数的返回值类型会通过返回值推断出来,也可以不指定。
可索引的类型
可索引的类型描述的是可以通过索引得到的类型。比如arr[1]或person.name。说白了就是数组或对象遵循的规范和约束。可索引类型具有一个索引签名,它描述了索引的类型,还有相应的索引返回值类型。
下例用接口表示数组:StringArray接口的索引签名类型为number,索引签名的返回值类型为string
interface StringArray {
  [index: number]: string;
}
const arr: StringArray = ['red', 'pink']
索引签名共有两种形式:数字和字符串
1. 数字:
interface Arr {
  [index: number]: number
}
const arr: Arr = [1,2,3]
2. 字符串:
interface PropString {
  [prop: string]: number
}
const objStr: PropString = {
   success: 1,
   failed: 0
}
可以将索引签名设置为只读,这样可以防止给索引赋值:
interface Sina {
  readonly [index: number]: string
}
const sian: Sina = {
  1: 'sina'
}
sian[1] = 9 // Index signature in type 'Sina' only permits reading
可以同时使用两种类型的索引,但是数字类型的索引对应的返回值必须是字符串类型索引返回值得子类型,因为当使用数字作为索引时,javascript会将数字转成字符串再去索引对象。
如下例:string类型属性的返回值为string类型,所以number类型属性的返回值必须为string的子类型(包括string)。
// 正确
interface PropType {
  [id: number]: string;
  [propName: string]: string;
}
// 正确
interface PropType {
  [id: number]: number;
  [propName: string]: any;
}
// 报错
interface PropType {
  [id: number]: number;
  [propName: string]: string;
}
// Numeric index type 'number' is not assignable to string index type 'string'.
注意:一旦定义了可索引签名,那么必选属性和可选属性的返回值类型都必须是它的类型返回值的子集:
下例由于 age 的类型与索引类型返回值的类型不匹配会报错:
interface Test {
  name: string;
  age?: number; // Property 'age' of type 'number' is not assignable to string index type 'string'.
  [prop: string]: string;
}
继承接口
和类一样,接口也可以继承。
interface BaseInfo {
  name: string,
  age: number
}
interface AddressInfo extends BaseInfo {
  address: string
}
const userInfo: AddressInfo = {
  name: 'jack',
  age: 12,
  address: 'shanghai'
}
继承多个接口:
interface AllInfo extends BaseInfo, AddressInfo {
  hobby: string
}
const allInfo: AllInfo = {
  name: 'mike',
  age: 12,
  address: 'beijing',
  hobby: 'song'
}
混合类型
函数也是对象,也有自己的属性和方法。
假设有一个变量,每执行一次函数变量加1:
JavaScript实现方式
- 定义全局变量:
 
let count = 0
const add = () => count++
以上方式count定义在全局作用域,很明显会作用域。使用闭包的方式:
- 闭包
 
const getCount = () => {
  let count = 0
  return () => count++
}
const add = getCount()
add()
- 函数属性
 
const getCount = () => {
  getCount.count++
}
getCount.count = 0
getCount()
通过TypeScript混合类型接口实现
interface Counter {
  // 函数参数和返回值类型
  (): void;
  // 函数属性及类型
  count: number;
  // 函数方法及返回值
  rest(): void;
}
// 定义一个函数,该函数返回Counter类型的函数
function getCounter(): Counter {
  const getCount = () => {
    getCount.count++
  }
  getCount.count = 0
  getCount.rest = function () {
    this.count = 0
  }
  return getCount
}
const getCount = getCounter()
getCount()        // 1
getCount()        // 2
getCount.rest()   // 0