TypeScript 聲明文件原理

2022-04-21 10:22 更新

聲明文件原理:深入探究

組織模塊以提供你想要的API形式保持一致是比較難的。 比如,你可能想要這樣一個模塊,可以用或不用 new來創(chuàng)建不同的類型, 在不同層級上暴露出不同的命名類型, 且模塊對象上還帶有一些屬性。

閱讀這篇指定后,你就會了解如果書寫復雜的暴露出友好API的聲明文件。 這篇指定針對于模塊(UMD)庫,因為它們的選擇具有更高的可變性。

核心概念

如果你理解了一些關于TypeScript是如何工作的核心概念, 那么你就能夠為任何結構書寫聲明文件。

類型

如果你正在閱讀這篇指南,你可能已經(jīng)大概了解TypeScript里的類型指是什么。 明確一下, 類型通過以下方式引入:

  • 類型別名聲明(type sn = number | string;
  • 接口聲明(interface I { x: number[]; }
  • 類聲明(class C { }
  • 枚舉聲明(enum E { A, B, C }
  • 指向某個類型的import聲明

以上每種聲明形式都會創(chuàng)建一個新的類型名稱。

與類型相比,你可能已經(jīng)理解了什么是值。 值是運行時名字,可以在表達式里引用。 比如 let x = 5;創(chuàng)建一個名為x的值。

同樣,以下方式能夠創(chuàng)建值:

  • letconst,和var聲明
  • 包含值的namespacemodule聲明
  • enum聲明
  • class聲明
  • 指向值的import聲明
  • function聲明

命名空間

類型可以存在于命名空間里。 比如,有這樣的聲明 let x: A.B.C, 我們就認為 C類型來自A.B命名空間。

這個區(qū)別雖細微但很重要 -- 這里,A.B不是必需的類型或值。

簡單的組合:一個名字,多種意義

一個給定的名字A,我們可以找出三種不同的意義:一個類型,一個值或一個命名空間。 要如何去解析這個名字要看它所在的上下文是怎樣的。 比如,在聲明 let m: A.A = A;, A首先被當做命名空間,然后做為類型名,最后是值。 這些意義最終可能會指向完全不同的聲明!

這看上去另人迷惑,但是只要我們不過度的重載這還是很方便的。 下面讓我們來看看一些有用的組合行為。

內置組合

眼尖的讀者可能會注意到,比如,class同時出現(xiàn)在類型列表里。 class C { }聲明創(chuàng)建了兩個東西: 類型C指向類的實例結構, C指向類構造函數(shù)。 枚舉聲明擁有相似的行為。

用戶組合

假設我們寫了模塊文件foo.d.ts:

export var SomeVar: { a: SomeType };
export interface SomeType {
  count: number;
}

這樣使用它:

import * as foo from './foo';
let x: foo.SomeType = foo.SomeVar.a;
console.log(x.count);

這可以很好地工作,但是我們知道SomeTypeSomeVar很相關 因此我們想讓他們有相同的名字。 我們可以使用組合通過相同的名字 Bar表示這兩種不同的對象(值和對象):

export var Bar: { a: Bar };
export interface Bar {
  count: number;
}

這提供了解構使用的機會:

import { Bar } from './foo';
let x: Bar = Bar.a;
console.log(x.count);

再次地,這里我們使用Bar做為類型和值。 注意我們沒有聲明 Bar值為Bar類型 -- 它們是獨立的。

高級組合

有一些聲明能夠通過多個聲明組合。 比如, class C { }interface C { }可以同時存在并且都可以做為C類型的屬性。

只要不產(chǎn)生沖突就是合法的。 一個普通的規(guī)則是值總是會和同名的其它值產(chǎn)生沖突除非它們在不同命名空間里, 類型沖突則發(fā)生在使用類型別名聲明的情況下( type s = string), 命名空間永遠不會發(fā)生沖突。

讓我們看看如何使用。

利用interface添加

我們可以使用一個interface往別一個interface聲明里添加額外成員:

interface Foo {
  x: number;
}
// ... elsewhere ...
interface Foo {
  y: number;
}
let a: Foo = ...;
console.log(a.x + a.y); // OK

這同樣作用于類:

class Foo {
  x: number;
}
// ... elsewhere ...
interface Foo {
  y: number;
}
let a: Foo = ...;
console.log(a.x + a.y); // OK

注意我們不能使用接口往類型別名里添加成員(type s = string;

使用namespace添加

namespace聲明可以用來添加新類型,值和命名空間,只要不出現(xiàn)沖突。

比如,我們可能添加靜態(tài)成員到一個類:

class C {
}
// ... elsewhere ...
namespace C {
  export let x: number;
}
let y = C.x; // OK

注意在這個例子里,我們添加一個值到C靜態(tài)部分(它的構造函數(shù))。 這里因為我們添加了一個 ,且其它值的容器是另一個值 (類型包含于命名空間,命名空間包含于另外的命名空間)。

我們還可以給類添加一個命名空間類型:

class C {
}
// ... elsewhere ...
namespace C {
  export interface D { }
}
let y: C.D; // OK

在這個例子里,直到我們寫了namespace聲明才有了命名空間C。 做為命名空間的 C不會與類創(chuàng)建的值C或類型C相互沖突。

最后,我們可以進行不同的合并通過namespace聲明。 Finally, we could perform many different merges usingnamespace declarations. This isn't a particularly realistic example, but shows all sorts of interesting behavior:

namespace X {
  export interface Y { }
  export class Z { }
}

// ... elsewhere ...
namespace X {
  export var Y: number;
  export namespace Z {
    export class C { }
  }
}
type X = string;

在這個例子里,第一個代碼塊創(chuàng)建了以下名字與含義:

  • 一個值X(因為namespace聲明包含一個值,Z
  • 一個命名空間X(因為namespace聲明包含一個值,Z
  • 在命名空間X里的類型Y
  • 在命名空間X里的類型Z(類的實例結構)
  • X的一個屬性值Z(類的構造函數(shù))

第二個代碼塊創(chuàng)建了以下名字與含義:

  • Ynumber類型),它是值X的一個屬性
  • 一個命名空間Z
  • Z,它是值X的一個屬性
  • X.Z命名空間下的類型C
  • X.Z的一個屬性值C
  • 類型X

使用export =import

一個重要的原則是exportimport聲明會導出或導入目標的所有含義

以上內容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號