【Typescript小手册】复合类型

1,473 阅读12分钟

概述

Typescript 的复合类型指由基本类型组成的类型,包括:

  • 数组
  • 枚举
  • 元组
  • 函数
  • 接口

类型

数组

Javascript 中的数组元素可以是相互不同的元素,但是 Typescript 中的数组元素类型必须一致(除非是any类型)。

有两种方式声明数组类型变量。示例:

let a: string[] = ['a', 'b', 'c']
let b: Array<string> = ['d', 'e', 'f']

前者是在其它语言中常见的中括号方式,后者是泛型的方式。

多维数组

两种方式都可以声明多维数组。示例:

let c: string[][] = [['a'], ['b'], ['c']]
let d: Array<Array<string>> = [['d'], ['e'], ['f']]

枚举

枚举是限定了值的范围的类型。

示例:

enum Character {
  a,
  b,
  c
}

let a: Character = Character.a

我们通过.来读取枚举值,枚举变量只能赋值给相同类型的枚举变量。

这一段代码被编译成 Javascript 后是这样的:

var Character
;(function (Character) {
  Character[(Character['a'] = 0)] = 'a'
  Character[(Character['b'] = 1)] = 'b'
  Character[(Character['c'] = 2)] = 'c'
})(Character || (Character = {}))
var a = Character.a

也就是说枚举值的本质是一个对象,也是一个名和值的互相映射。枚举值的内部值可以自定义,详情请看 Typescript 文档:enums

常量枚举

如果不希望枚举类型被编译成 Javascript,可以使用常量枚举,它会将枚举的真实值保存到使用处。

示例:

enum Character {
  a,
  b,
  c
}

let a: Character = Character.a

编译后:

var a = 0 /* a */

元组

元素是一种类似数组,但是元素数量固定且类型可以不同的类型。

示例:

let a: [string, number] = ['a', 0]

let b: string = a[0]
let c: number = a[1]

元素的类型必须按声明顺序初始化。它有数组的属性和方法,使用方法和数组相似,但是不能读取和写入超出长度范围的元素。

元组可以用问号?声明可选元素。示例:

let a: [string, number?, number?]

可选元素后的元素也必须是可选元素。

函数

Javascript 中定义函数的方式有function关键词和箭头(=>)函数,这两种方式都会有参数和返回值类型,Typescript 把参数的数量和类型以及返回值类型的组合称为函数的类型。

两种函数声明类型的方式分别是:

function b(x: string): number {
  return x.length
}

let a = (x: string): number => x.length

函数的返回值类型如果可以被编译器自动推断,那么可以省略。比如:

function b(x: string) {
  return x.length
}

因为参数x的属性lengthnumber类型,且函数体没有其它分支结构,所以编译器自动推断函数返回值类型是number

一个变量可以用箭头函数的方式来声明为一个函数类型。比如:

let a: (x: string) => number
a = (x: string): number => x.length

可选参数

函数参数可以用问号?来声明为可选,这个特性和 Javascript 是一致的,即可选参数之后的参数也必须是可选参数。示例:

function c(x: string, y?: string, z?: string) {}

需要注意的是,Typescript 中的可选参数不能与 Javascript 的参数默认值同时使用。

this

在严格模式下,函数内不允许有any类型的this指针。比如一个孤立的函数:

function a() {
  return this.value // 错误
}

我们可以在函数参数列表的第一个“参数”声明this的类型。示例:

function a(this: { value: number }) {
  console.log(this.value)
}

这里的this并不是真的参数,只是一个声明。其余参数跟随其后,可以当作没有这个this

在对象或类中的函数,编译器可以自动推断其类型,不需要显式声明。

{value: number}是隐式接口类型,会在之后介绍。

构造方法

在函数类型声明中new关键词可以表示构造方法。示例:

const A: new (x: string) => { length: number } = class {
  length: number
  constructor(x: string) {
    this.length = x.length
  }
}

let a = new A('a')

这一段代码有点复杂,主要分为三个部分。第一部分是整体的赋值语句:const a: /* type */ = /* value */。其中的/* type */就是指函数类型:new (x: string) => { length: number },表示一个构造函数。最后/* value */是被赋值的类。

有关类class的内容,会在之后介绍。

异步函数

异步函数的返回值为Promise<T>,其中T为传递数据的类型。

示例:

async function fetch(): Promise<string> {
  return 'data'
}

接口

Typescript 与其它面向对象语言中的接口类似,用于声明一个具有特定属性和方法的对象的类型,但不需要具体实现。

示例:

interface A {
  name: string
  value: number
  operation: (x: string) => number
}

let a: A = {
  name: 'a',
  value: 0
  operation(x: string){
    return a.length
  }
}

给一个接口类型的变量赋值时,等号右边的变量必须满足接口的成员要求。

隐式接口

我们不一定需要使用interface关键词来显式地声明一个接口,因为有的时候有些接口我们只使用一次。我们可以在给变量赋值的时候隐式地声明接口,编译器会自动推断变量的类型。比如:

let a = {name: 'a'  value: 0}

这里,编译器自动推断变量a的类型为{name: string, value: number}

可选成员

成员后可接一个问号?表示这是一个可选成员。示例:

interface A {
  name: string
  value?: number
}

let a: A = { name: 'a' }

接口常常隐式出现,比如声明函数参数类型:

function log(x: { name: string; value: number }) {
  console.log('The value of', name, 'is', value)
}

这里的隐式接口表明参数x的类型是有namevalue成员的对象。

只读成员

通过修饰符readonly可以声明一个接口成员是只读的。

示例:

interface A {
  readonly name: string
  value: number
}

let a: A = { name: 'a', value: 0 }
a.name = 'b' // 错误

索引

Javascript 中可以通过中括号加字符串的方式读取对象的成员,Typescript 中也可以但是对此增加了限制。

在 Javascript 中,我们可以这样:

let a = { name: 'a', value: 0 }
let name = a['name']
a['text'] = 'A'

但是在 Typescript 中,a['text'] = 'A'语句会报错,因为 a 的类型中没有text成员。

如果我们希望一个对象可以动态地添加任意名称的成员,可以使用索引成员。

示例:

interface A {
  name: string
  [index: string]: string
}

let a: A = { name: 'a' }
a['name'] = 'a'

这里的[index: string]: string就是索引的声明,其中index是索引名,可以任意设置,中括号内的string是索引名类型,中括号外的string是索引值类型。示例中的接口A声明了字符串类型的索引,所以我们可以通过中括号加成员名的方式来动态添加在接口声明中没有的成员。

需要注意的是,索引的名称类型(示例中的index的类型)只能是stringnumber

因为这里声明了字符串类型的索引,所以编译器推断所有从该类型变量读取的成员都应该是字符串,因为对象的任何成员我们都可以用索引的方式获取,都应该遵守索引的规定。所以当成员的类型不是字符串时,编译器会报错。示例:

interface A {
  name: string
  value: number // 错误
  [index: string]: string
}

要解决这个问题,我们可以使用一个联合类型,表示索引值类型是stringnumber。示例:

interface A {
  name: string
  value: number
  [index: string]: string | number
}

或使用交叉类型。示例:

type A = {
  name: string
  [index: string]: string
} & { value: number }

索引名也可以被限制为在一组值中,这需要用到in操作符。示例:

type Keys = 'x' | 'y' | 'z'
type A = {
  [key in Keys]: string
}

有关联合和交叉的内容将在之后介绍。

继承

一个接口可通过extends关键词来继承另一个(或多个)接口,继承后的接口拥有被继承接口的全部成员。

示例:

interface A {
  a: string
}
interface B {
  b: string
}

interface C extends A, B {
  c: string
}

let c: C = { a: 'a', b: 'b', c: 'c' }

接口表示函数

接口可以表示函数类型。

示例:

interface A {
  (x: string): number
}

const a: A = (x: string): number => x.length

其中括号内是参数类型,括号后是返回值类型。注意其与函数成员的区别:没有方法名。

接口也可以表示构造方法。示例:

interface A {
  new (x: string): { length: number }
}

const ClassA: A = class {
  length: number

  constructor(x: string) {
    this.length = x.length
  }
}

类用于声明一个对象类型,即可以表示一个类型,也可以使用new操作符来实例化对象。它在成员声明时具有接口的所有特性,但是必须有具体的实现。

示例:

class A {
  name: string
  length: number = 0

  constructor(x: string) {
    this.name = x
    this.length = x.length
  }

  log(): void {
    console.log('The length of', this.name, 'is', this.length)
  }
}

let a: A = new A('a')

Typescript 在原本 Javascript 类的基础上添加了对类成员的支持,类体中最上面的两行就是 Javascript 类中不支持的成员成员语法。

需要注意的是,与接口不同的是,类既可以作为类型,又可以作为一个变量(本质是构造函数)。当类被赋值到一个变量上是,只有作为变量的部分被赋值,改变量无法作为类型使用。比如:

class A {
  name: string = 'a'
}

const B = A
let b: B = new B() // 错误

变量B被赋值为一个类,但是因为B是变量,它可以作为构造函数被调用,但不能用来声明变量b的类型。

权限修饰符

可以使用publicprotectedprivate来表示成员的访问权限,默认是public。它们的含义与一般的面向对象语言类似,public所有地方都可以访问,protected表示只有类内核派生类可以访问,private表示只有类自身可以访问。

示例:

class A {
  name: string = 'name'
  private length: number = 0
}

需要注意的是,Typescript 类成员的权限只会在编译时时检查,在生成的 Javascript 代码中,成员都是可访问的。

可选成员

与接口一样,类可以声明可选成员。示例:

class A {
  name?: string
}

只读成员

与接口一样,类可以声明只读成员。示例:

class A {
  readonly name: string = 'a'
}

只读属性可以在构造方法中初始化,同时只有在构造方法中可以改变值,在类的其它方法和类外都不能修改值。示例:

class A {
  readonly name: string = 'a'

  constructor() {
    this.name = 'b'
  }
}

成员

通过static关键词可以声明一个静态成员。示例:

class A {
  static value: string = 'A'
  static log() {
    console.log(A.value)
  }
}
A.log()

由于 Javascript 的类本质是构造函数,所以一些成员名称无法作为静态成员的名称,包括:

  • name
  • length
  • call

属性初始化

除了可选属性外,属性需要被初始化,可以通过属性列表或构造函数。但如果要表明某个属性不需要初始化,可以在属性名后添加感叹号!操作符。示例:

class A {
  name!: string
}

请注意这里与可选属性的区别,对于可选属性来说,类实例可以没有这个属性,比如:

class A {
  name?: string
}

let a: A = {}

class B {
  name!: string
}

let b: B = {} // 错误

这里将一个空对象赋值给一个A类型,因为A类型的name属性时可选的,所以空对象中即使没有name属性也可以赋值给A类型变量。对于 B 类型,虽然name属性不需要初始化,但是其对应的实例必须有name属性。

实现接口

类除了可以使用extends关键字来继承其它类(与 Javascript 一致)外,还可以使用implements关键词来实现一个或多个接口。

示例:

interface A {
  name: string
}
interface B {
  value: number
}

class C implements A, B {
  name: string = 'c'
  value: number = 0
}

一个类实现了接口,就意味着类必须具有接口声明的成员以及成员要求。比如:

interface A {
  readonly name: string
}
interface B {
  value: number
}

class C implements A, B {
  name: string = 'c'
  value?: number = 0 // 错误
}

let c: C = { name: 'c', value: 0 }
C.name = 'cc' // 错误

因为A接口的name成员是只读的,所以C类的name成员也隐式地具有只读成员。因为B接口的value成员非可选,所以C类的value成员不能是可选的。

同名函数

Typescript 中没有真正的函数重载,派生类的函数类型必须与基类中同名函数的类型兼容。比如:

class A {
  name: string = 'A'
  log() {
    console.log(this.name)
  }
}
class B extends A {
  name: string = 'B'
  log(x: number) {
    console.log(x)
  }
}

A中的log方法不接受参数,类B是类A的派生类,方法log接受一个参数,两者类型不兼容,编译器会报错。

如果将类Blog方法的参数改为可选参数,则两者可以兼容。示例:

class B extends A {
  name: string = 'B'
  log(x?: number) {
    console.log(x ? x : this.name)
  }
}

this 作为参数类型和返回值类型

this可以作为参数类型和返回值类型。示例:

class A {
  compare(other: this): this {
    if (other === this) {
      return this
    } else {
      return other
    }
  }
}

函数头中的this表示一个类型,函数体中的this表示一个指向自身的指针。

如果A的派生类调用了compare方法,那么此时函数头中的this表示的是派生类。比如:

class B extends A {
  value: number
}

let a: A = new A()
let b: B = new B()

b.compare(a) // 错误

因为派生类B中多了成员value所以A类与B类型不兼容,且在b调用compare方法时,参数other的类型this指的是B,所以a不能作为参数。也就是说,对于派生类来说,compare方法的函数类型相当于:

compare(other: B): B

抽象类

可以用关键字abstract声明一个抽象类,抽象类中可以声明抽象方法,即不需要具体实现的方法。

示例:

abstract class A {
  name: string = 'a'
  abstract log(): void
}

抽象方法不需要函数体,只需要参数类型和返回值类型。抽象类不能被直接实例化,必须被继承并在派生类中实现了抽象方法后,由子类来实例化。示例:

class B extends A {
  log(): void {
    console.log(this.name)
  }
}
let b: B = new B()