typescript学习-声明合并

125 阅读3分钟

1. 接口合并

在typescript,想要描述一个对象的类型。一般通过2种方式:

  • 使用type声明一个类型别名
  • 使用interface声明一个接口

上述2种方式在使用上几乎可以相互替换。最主要的一点不同在于type不能进行声明合并,而interface可以进行声明合并。如下:

interface Animal {}
interface Sheep {}
interface Dog {}
interface Cat {}

interface Cloner {
  clone(animal: Animal): Animal;
  print():void
}
interface Cloner {
  clone(animal: Sheep): Sheep;
}
interface Cloner {
  clone(animal: Dog): Dog;
  clone(animal: Cat): Cat;
}

interface Cloner {  // 合并后的结果
  clone(animal: Dog): Dog;
  clone(animal: Cat): Cat;
  clone(animal: Sheep): Sheep;
  clone(animal: Animal): Animal;
  print():void
}

简单来说,就是同名接口上的所有成员全部都被合并到一个接口内部,并遵循以下规则:

  • 同名方法,变为重载
  • 同名属性必须类型一致

这儿还有一个隐式规则,就是后定义的接口,优先级最高。比如上述示例,第三个Cloner接口的优先级最高,其clone方法在合并接口里的位置是在最顶端。

2. 命名空间合并

  • 命名空间之间的合并

    简洁的说,就是将2个命名空间内部的的类型声明以及值合并到一起,此时需要遵守变量“单一声明”原则,比如class不能重复定义。这个地方可以这样理解,当我们编写不带命名空间的代码时,默认处于default命名空间下,此时需要遵守的变量声明原则,和命名空间合并保持一致。

  • 命名空间与class,function,enum的合并

    命名空间的合并是灵活的,其可以与class,function,enum进行合并,当采用这类型的合并时,同时兼并命名空间和指定合并类型的特性。

    • 与class合并

      class Demo {
        name="Demo"
      }
      namespace Demo {
        export class A {
          name= "A"
        }
      }
      let demo = new Demo()
      let demoA = new Demo.A()
      
    • 与function合并

      function Demo(){
        console.log('demo')
      }
      namespace Demo {
        let a = 'a'
        export function doA(){
          console.log(a)
        }
      }
      Demo()
      Demo.doA()
      
    • 与enum合并

      enum Demo {
        First,
        Second
      }
      namespace Demo {
        let a = 'a'
        export function doA(){
          console.log(a)
        }
      }
      console.log(Demo.First)
      console.log(Demo.doA)
      

3. 模块扩展

虽然javascript的模块不支持合并,但依旧可以引入已经存在的对象,并进行修改,简单示例如下:

// observable.ts
export class Observable<T> {
  // ... implementation left as an exercise for the reader ...
}
// map.ts
import { Observable } from "./observable";
Observable.prototype.map = function (f) {
  // ... another exercise for the reader
};

通过上面的方式,依然可以对class类进行扩展,新增一个map方法。但这种方式在typescript中不会得到正确的类型推导,即typescript并不知道Observable类拥有map方法。那么此时就可以使用模块扩展,即扩展Observable的声明定义,语法:declare module modulePath。如下:

// observable.ts
export class Observable<T> {
  // ... implementation left as an exercise for the reader ...
}
// map.ts
import { Observable } from "./observable";
declare module "./observable" {
  interface Observable<T> {
    map<U>(f: (x: T) => U): Observable<U>;
  }
}

通过类型扩展,此时typescript就会知道Observable类拥有一个map方法,类型扩展必须遵顼2个原则:

  • 不能扩展一个新的顶层声明,即只能针对已经存在的类型声明进行扩展。比如在上述模块文件./observable.ts中,只导出了一个Observable,则此时不能新增一个NewObservable的扩展声明。
  • 默认导出不能被扩展。因为扩展需要基于一个导出名称。

4. 全局扩展

全局扩展和模块扩展在特性上保持一致。只在语法上略有区别,如下:

// observable.ts
export class Observable<T> {
  // ... still no implementation ...
}
declare global {
  interface Array<T> {
    toObservable(): Observable<T>;
  }
}
Array.prototype.toObservable = function () {
  // ...
};

语法:declare global。上述示例即在内置类型Array上新增了一个toObservable的方法。