愿你坚持不懈,努力进步,进阶成自己理想的人

—— 2017.09, 写给3年后的自己

Typescript学习记录:接口

一、介绍

typescript的核心设计原则之一是对值所具有的结构进行类型检查,接口的作用就是为类型命名和为代码结构定义契约,一个简单例子如:

function showName(person: { name: string; age: number }) {
    console.log(person.name)
}

这里限定:传给showName()函数的参数person,必须具有{ name: string, age: number }的结构,所以使用如下:

let p = {
    name: 'RuphiLau',
    age: 21
}
showName(p)

如果变量p没有完整具有nameage两个相应类型的属性,就会报错,我们可以将限定p类型的结构起个名称为Person,定义接口如下:

interface Person {
    name: string
    age: number
}

从而改写如下:

function showName(person: Person) {
    // ...
}


二、可选属性

可选属性允许我们可以只实现接口里的部分定义,用法为在名称后面加上?,如:

interface Person {
    name: string
    age: number
    isCoder?: boolean
}

此时,以下两种写法都是合法的:

let p: Person = {
    name: 'RuphiLau',
    age: 21
}
let p2: Person = {
    name: 'RuphiLau',
    age: 21,
    isCoder: true
}


三、只读属性

如果一个属性只能在刚刚创建的时候修改值,那么可以在属性名前用readonly来指定只读属性,如:

interface Point {
    readonly x: number
    readonly y: number
}

使用如下:

let p: Point = { x: 10, y: 20 }
p.x = 100 // 报错

注意:typescript中有ReadonlyArray<T>类型,它与Array<T>类型类似,只不过创建过后,就不允许数组再修改了,如:

let arr: number[] = [1, 2, 3, 4]
let roArr: ReadonlyArray<number> = arr
roArr.push(5) // 报错
roArr.length = 5 // 报错

即使再把ReadonlyArray类型的数组赋给普通类型的数组,也是不行的,但是可以使用类型断言,如:

arr = roArr // 报错
roArr as number[] // 可以

readonly vs const?
一般情况下,readonly用于属性,而const则用于变量


四、额外的属性检查

当一个对象字面量里声明了接口中不存在的属性时,会报错不存在错误,如:

interface Demo {
    a: number
    b: number
}
let d: Demo = {
    a: 1,
    b: 2,
    c: 3
}
// 报错: Object literal may only specify known properties, and 'c' does not exist in type 'Demo'.

这是因为TS会对对象字面量会进行额外的属性检查,解决这个问题,可以使用类型断言,如:

let d: Demo = {a:1, b:2, c:3} as Demo

更好的方法,则是使用字符串索引签名,如:

interface Demo {
    a: number
    b: number
    [c: string]: any
}


五、类型签名

接口能够描述JavaScript中对象拥有的各式各样的外形,除了描述带有属性的普通对象外,还可以描述:

1、描述函数

可以给接口定义一个调用签名,如:

interface MyFn {
    (a: number, b: number): boolean
}

let fn: MyFn
fn = function(a: number, b: number): boolean {
    return a > b
}

需要注意的是:函数的参数名并不需要和接口定义里的参数名相匹配,只要类型是兼容的就可以了

2、描述索引

可以描述索引,如:

interface StringArray {
    [index: string]: string
}

let x: StringArray
x.name = 'RuphiLau' // 可以
x.age = 21 // 报错

索引签名支持两种类型:numberstring,但是由于number实际上会被转化为string类型(根据对象key的性质),所以需要遵守:number索引的返回值类型是string索引的返回值类型的子类型,所以以下的做法是错误的:

class A {
    propA: string
}
class B extends A {
    propB: string
}

interface Demo {
    [index: number]: A
    [index: string]: B
}
// 报错:Numeric index type 'A' is not assignable to string index type 'B'.

如果interface里还声明了一个和索引签名索引返回值类型不匹配的属性,会报错,如下:

interface Demo {
    [index: string]: string
    name: string // 没问题,因为返回值类型是string
    age: number // 报错,因为返回值类型是number,不符合string类型
}

最后,我们还可以声明一个readonly的索引签名,如:

interface ReadonlyStringArray {
    readonly [index: number]: string
}
let x: ReadonlyStringArray = ['RuphiLau']
x[0] = 'RuphiLau'
// 报错:Index signature in type 'ReadonlyStringArray' only permits reading.


六、类类型

typescript里也允许像Java、C#那样,让一个class去实现一个interface,如:

interface ISome {
    prop: string // 描述一个属性
    method(paramA: string, paramB: number) // 描述一个方法
}
class A implements ISome {
    prop: 'propValue'
    method(a: string, b: number) {
        // ...
    }
    constructor(paramA: number){}
}

但是需要注意的是,接口描述的是类的公共部分,而不是公共和私有两部分,所以不会检查类是否具有某些私有成员。

1、静态部分实例部分

首先看一个示例:用构造器签名定义一个接口,并试图实现这个接口:

interface Person {
    new(name: string)
}

class People implements Person {
    constructor(name: string) {
        // ...
    }
}
// 报错:no match for the signature 'new (name: string): any'.

这是因为:当类实现一个接口时,只对实例部分进行类型检查,而constructor存在于静态部分,所以不在检查的范围内
所以做法如下:

// 针对类构造函数的接口
interface CPerson {
    new(name: string)
}
// 针对类的接口
interface IPerson {
    name: string
    age: number
}

function create(c: CPerson, name: string): IPerson {
    return new c(name)
}

class People implements IPerson {
    name: string
    age: number
}

let p = create(People, 'RuphiLau') // 可以

2、混合类型

允许让一个对象同时作为函数和对象使用,并带有额外的属性,如:

interface MixedDemo {
    (str: string): void
    defaultStr: string
}

function foo(): MixedDemo {
    let x = <MixedDemo>function(str: string){
        console.log(str)
    }
    x.defaultStr = 'Hello, world'
    return x
}

let c = foo()
c('This is a function') // 输出:'This is a function'
console.log(c.defaultStr) // 输出:'Hello, world'

3、接口继承类

接口可以继承自一个类,从而像声明了所有类中存在的成员,并且privateprotected成员也会被继承,这意味着:只有类自己或子类能够实现该接口,例子如:

class A {
    protected propA: string
}
interface I extends A {
    method(): void
}

// 下面这种做法会报错
class C implements A {
    protected propA: string
    method() {}
}

// 下面这种做法则是允许的
class C extends A implements A {
    protected propA: string
    method() {}
}