基礎類型
布爾值
最基本的數據類型就是簡單的true/false值,在JavaScript和TypeScript裏叫作boolean。
let flag: boolean = false;
數字
和JavaScript同樣,TypeScript裏的全部數字都是浮點數。 這些浮點數的類型是number。 除了支持十進制和十六進制字面量,TypeScript還支持ECMAScript 2015中引入的二進制和八進制字面量。
let decLiteral: number = 6;
let hexLiteral: number = 0xf00d;
let binaryLiteral: number = 0b1010;
let octalLiteral: number = 0o744;
字符串
JavaScript程序的另外一項基本操做是處理網頁或服務器端的文本數據。 像其它語言裏同樣,咱們使用string表示文本數據類型。 和JavaScript同樣,能夠使用雙引號(")或單引號(')表示字符串。
let name: string = "bob";
name = "smithv;
模版字符串
let name: string = Gene
;
let age: number = 37;
let sentence: string = Hello, my name is ${ name }.<br/>I'll be ${ age + 1 } years old next month.
;
數組
TypeScript像JavaScript同樣能夠操做數組元素。 有兩種方式能夠定義數組。 第一種,能夠在元素類型後面接上[],表示由此類型元素組成的一個數組:
let list: number[] = [1, 2, 3];
第二種方式是使用數組泛型,Array<元素類型>:
let list: Array<number> = [1, 2, 3];
元組 Tuple
元組類型容許表示一個已知元素數量和類型的數組,各元素的類型沒必要相同。 好比,你能夠定義一對值分別爲string和number類型的元組。
let x: [string, number];
x = ['hello', 10];
x = [10, 'hello'];
當訪問一個已知索引的元素,會獲得正確的類型:
console.log(x[0].substr(1));
console.log(x[1].substr(1));
當訪問一個越界的元素,會使用聯合類型替代:
x[3] = 'world'; // OK, 字符串能夠賦值給(string | number)類型
console.log(x[5].toString());
x[6] = true;
枚舉
enum類型是對JavaScript標準數據類型的一個補充。
enum Color {Red, Green, Blue}
let c: Color = Color.Green;
任意值
有時候,咱們會想要爲那些在編程階段還不清楚類型的變量指定一個類型。 這些值可能來自於動態的內容,好比來自用戶輸入或第三方代碼庫。 這種狀況下,咱們不但願類型檢查器對這些值進行檢查而是直接讓它們經過編譯階段的檢查。 那麼咱們能夠使用any類型來標記這些變量:
let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false;
在對現有代碼進行改寫的時候,any類型是十分有用的,它容許你在編譯時可選擇地包含或移除類型檢查。 你可能認爲Object有類似的做用,就像它在其它語言中那樣。 可是Object類型的變量只是容許你給它賦任意值 - 可是卻不可以在它上面調用任意的方法,即使它真的有這些方法:
let notSure: any = 4;
notSure.ifItExists();
notSure.toFixed();
let prettySure: Object = 4;
prettySure.toFixed();
let list: any[] = [1, true, "free"];
list[1] = 100;javascript
空值
某種程度上來講,void類型像是與any類型相反,它表示沒有任何類型。 當一個函數沒有返回值時,你一般會見到其返回值類型是void:
function test(): void {
alert("This is my warning message");
}
聲明一個void類型的變量沒有什麼大用,由於你只能爲它賦予undefined和null:
let unusable: void = undefined;
Null 和 Undefined
TypeScript裏,undefined和null二者各自有本身的類型分別叫作undefined和null。 和void類似,它們的自己的類型用處不是很大:
let u: undefined = undefined;
let n: null = null;
默認狀況下null和undefined是全部類型的子類型。 就是說你能夠把null和undefined賦值給number類型的變量。
然而,當你指定了--strictNullChecks標記,null和undefined只能賦值給void和它們各自。 這能避免不少常見的問題。 也許在某處你想傳入一個string或null或undefined,你能夠使用聯合類型string | null | undefined。 再次說明,稍後咱們會介紹聯合類型。
Never
never類型表示的是那些永不存在的值的類型。 例如,never類型是那些老是會拋出異常或根本就不會有返回值的函數表達式或箭頭函數表達式的返回值類型; 變量也多是never類型,當它們被永不爲真的類型保護所約束時。
never類型是任何類型的子類型,也能夠賦值給任何類型;然而,沒有類型是never的子類型或能夠賦值給never類型(除了never自己以外)。 即便any也不能夠賦值給never。
下面是一些返回never類型的函數:html
function error(message: string): never {
throw new Error(message);
}java
function fail() {
return error("Something failed");
}node
function infiniteLoop(): never {
while (true) {
}
}react
類型斷言 有時候你會遇到這樣的狀況,你會比TypeScript更瞭解某個值的詳細信息。 一般這會發生在你清楚地知道一個實體具備比它現有類型更確切的類型。
經過類型斷言這種方式能夠告訴編譯器,「相信我,我知道本身在幹什麼」。 類型斷言比如其它語言裏的類型轉換,可是不進行特殊的數據檢查和解構。 它沒有運行時的影響,只是在編譯階段起做用。 TypeScript會假設你,程序員,已經進行了必須的檢查。
類型斷言有兩種形式。 其一是「尖括號」語法:
let someValue: any = "this is a string";jquery
let strLength: number = (<string>someValue).length;
另外一個爲as語法:
let someValue: any = "this is a string";git
let strLength: number = (someValue as string).length;程序員
變量聲明
let和const是JavaScript裏相對較新的變量聲明方式。 像咱們以前提到過的,let在不少方面與var是類似的,可是能夠幫助你們避免在JavaScript裏常見一些問題。 const是對let的一個加強,它能阻止對一個變量再次賦值。
由於TypeScript是JavaScript的超集,因此它自己就支持let和const。 下面咱們會詳細說明這些新的聲明方式以及爲何推薦使用它們來代替var。
若是你以前使用JavaScript時沒有特別在乎,那麼這節內容會喚起你的回憶。 若是你已經對var聲明的怪異之處瞭如指掌,那麼你能夠輕鬆地略過這節。
var 聲明
一直以來咱們都是經過var關鍵字定義JavaScript變量。
var a = 10;
你們都能理解,這裏定義了一個名爲a值爲10的變量。
咱們也能夠在函數內部定義變量:
function f() {
var message = "Hello, world!";正則表達式
return message;
}
而且咱們也能夠在其它函數內部訪問相同的變量。
function f() {
var a = 10;
return function g() {
var b = a + 1;
return b;
}
}算法
var g = f();
g();
上面的例子裏,g能夠獲取到f函數裏定義的a變量。 每當g被調用時,它均可以訪問到f裏的a變量。 即便當g在f已經執行完後才被調用,它仍然能夠訪問及修改a。
function f() {
var a = 1;
a = 2; var b = g(); a = 3; return b; function g() { return a; }
}
f();
做用域規則
對於熟悉其它語言的人來講,var聲明有些奇怪的做用域規則。 看下面的例子:
function f(shouldInitialize: boolean) {
if (shouldInitialize) {
var x = 10;
}
return x;
}
f(true);
f(false);
這是由於var聲明能夠在包含它的函數,模塊,命名空間或全局做用域內部任何位置被訪問(咱們後面會詳細介紹),包含它的代碼塊對此沒有什麼影響。 有些人稱此爲var做用域或函數做用域。 函數參數也使用函數做用域。
這些做用域規則可能會引起一些錯誤。 其中之一就是,屢次聲明同一個變量並不會報錯:
function sumMatrix(matrix: number[][]) {
var sum = 0;
for (var i = 0; i < matrix.length; i++) {
var currentRow = matrix[i];
for (var i = 0; i < currentRow.length; i++) {
sum += currentRow[i];
}
}
return sum;
}
這裏很容易看出一些問題,裏層的for循環會覆蓋變量i,由於全部i都引用相同的函數做用域內的變量。 有經驗的開發者們很清楚,這些問題可能在代碼審查時漏掉,引起無窮的麻煩。
let 聲明
如今你已經知道了var存在一些問題,這剛好說明了爲何用let語句來聲明變量。 除了名字不一樣外,let與var的寫法一致。
let hello = "Hello!";
塊做用域
當用let聲明一個變量,它使用的是詞法做用域或塊做用域。 不一樣於使用var聲明的變量那樣能夠在包含它們的函數外訪問,塊做用域變量在包含它們的塊或for循環以外是不能訪問的。
function f(input: boolean) {
let a = 100;
if (input) {
let b = a + 1;
return b;
}
return b;
}
這裏咱們定義了2個變量a和b。 a的做用域是f函數體內,而b的做用域是if語句塊裏。
在catch語句裏聲明的變量也具備一樣的做用域規則。
try {
throw "oh no!";
}
catch (e) {
console.log("Oh well.");
}
console.log(e);
擁有塊級做用域的變量的另外一個特色是,它們不能在被聲明以前讀或寫。 雖然這些變量始終「存在」於它們的做用域裏,但在直到聲明它的代碼以前的區域都屬於暫時性死區。 它只是用來講明咱們不能在let語句以前訪問它們,幸運的是TypeScript能夠告訴咱們這些信息。
a++;
let a;
注意一點,咱們仍然能夠在一個擁有塊做用域變量被聲明前獲取它。 只是咱們不能在變量聲明前去調用那個函數。 若是生成代碼目標爲ES2015,現代的運行時會拋出一個錯誤;然而,現今TypeScript是不會報錯的。
function foo() {
return a;
}
foo();
let a;
重定義及屏蔽
function f(x) {
var x;
var x;
if (true) { var x; }
}
在上面的例子裏,全部x的聲明實際上都引用一個相同的x,而且這是徹底有效的代碼。 這常常會成爲bug的來源。 好的是,let聲明就不會這麼寬鬆了。
let x = 10;
let x = 20; // 錯誤,不能在1個做用域裏屢次聲明x
並非要求兩個均是塊級做用域的聲明TypeScript纔會給出一個錯誤的警告。
function f(x) {
let x = 100; // error: interferes with parameter declaration
}
function g() {
let x = 100;
var x = 100;
}
並非說塊級做用域變量不能用函數做用域變量來聲明。 而是塊級做用域變量須要在明顯不一樣的塊裏聲明。
function f(condition, x) {
if (condition) {
let x = 100;
return x;
}
return x;
}
f(false, 0); // returns 0
f(true, 0); // returns 100
在一個嵌套做用域裏引入一個新名字的行爲稱作屏蔽。 它是一把雙刃劍,它可能會不當心地引入新問題,同時也可能會解決一些錯誤。 例如,假設咱們如今用let重寫以前的sumMatrix函數。
function sumMatrix(matrix: number[][]) {
let sum = 0;
for (let i = 0; i < matrix.length; i++) {
var currentRow = matrix[i];
for (let i = 0; i < currentRow.length; i++) {
sum += currentRow[i];
}
}
return sum;
}
塊級做用域變量的獲取
在咱們最初談及獲取用var聲明的變量時,咱們簡略地探究了一下在獲取到了變量以後它的行爲是怎樣的。 直觀地講,每次進入一個做用域時,它建立了一個變量的環境。 就算做用域內代碼已經執行完畢,這個環境與其捕獲的變量依然存在。
function theCityThatAlwaysSleeps() {
let getCity;
if (true) { let city = "Seattle"; getCity = function() { return city; } } return getCity();
}
由於咱們已經在city的環境裏獲取到了city,因此就算if語句執行結束後咱們仍然能夠訪問它。
回想一下前面setTimeout的例子,咱們最後須要使用當即執行的函數表達式來獲取每次for循環迭代裏的狀態。 實際上,咱們作的是爲獲取到的變量建立了一個新的變量環境。 這樣作挺痛苦的,可是幸運的是,你沒必要在TypeScript裏這樣作了。
當let聲明出如今循環體裏時擁有徹底不一樣的行爲。 不只是在循環裏引入了一個新的變量環境,而是針對每次迭代都會建立這樣一個新做用域。 這就是咱們在使用當即執行的函數表達式時作的事,因此在setTimeout例子裏咱們僅使用let聲明就能夠了。
for (let i = 0; i < 10 ; i++) {
setTimeout(function() {console.log(i); }, 100 * i);
}
const 聲明
const 聲明是聲明變量的另外一種方式。
const numLivesForCat = 9;
它們與let聲明類似,可是就像它的名字所表達的,它們被賦值後不能再改變。 換句話說,它們擁有與let相同的做用域規則,可是不能對它們從新賦值。
這很好理解,它們引用的值是不可變的。
const numLivesForCat = 9;
const kitty = {
name: "Aurora",
numLives: numLivesForCat,
}
// Error
kitty = {
name: "Danielle",
numLives: numLivesForCat
};
kitty.name = "Rory";
kitty.name = "Kitty";
kitty.name = "Cat";
kitty.numLives--;
除非你使用特殊的方法去避免,實際上const變量的內部狀態是可修改的。 幸運的是,TypeScript容許你將對象的成員設置成只讀的。 接口一章有詳細說明。
let vs. const
如今咱們有兩種做用域類似的聲明方式,咱們天然會問到底應該使用哪一個。 與大多數泛泛的問題同樣,答案是:依狀況而定。
使用最小特權原則,全部變量除了你計劃去修改的都應該使用const。 基本原則就是若是一個變量不須要對它寫入,那麼其它使用這些代碼的人也不可以寫入它們,而且要思考爲何會須要對這些變量從新賦值。 使用const也可讓咱們更容易的推測數據的流動。
另外一方面,用戶很喜歡let的簡潔性。 這個手冊大部分地方都使用了let
解構
Another TypeScript已經能夠解析其它 ECMAScript 2015 特性了。 完整列表請參見 the article on the Mozilla Developer Network。 本章,咱們將給出一個簡短的概述。
解構數組
最簡單的解構莫過於數組的解構賦值了:
let input = [1, 2];
let [first, second] = input;
console.log(first); // outputs 1
console.log(second); // outputs 2
這建立了2個命名變量 first 和 second。 至關於使用了索引,但更爲方便:
first = input[0];
second = input[1];
解構做用於已聲明的變量會更好:
// swap variables
[first, second] = [second, first];
做用於函數參數:
function f([first, second]: [number, number]) {
console.log(first);
console.log(second);
}
f(input);
你能夠在數組裏使用...語法建立剩餘變量:
let [first, ...rest] = [1, 2, 3, 4];
console.log(first);
console.log(rest);
固然,因爲是JavaScript, 你能夠忽略你不關心的尾隨元素:
let [first] = [1, 2, 3, 4];
console.log(first);
或其它元素:
let [, second, , fourth] = [1, 2, 3, 4];
對象解構
你也能夠解構對象:
let o = {
a: "foo",
b: 12,
c: "bar"
};
let { a, b } = o;
這經過 o.a and o.b 建立了 a 和 b 。 注意,若是你不須要 c 你能夠忽略它。
就像數組解構,你能夠用沒有聲明的賦值:
({ a, b } = { a: "baz", b: 101 });
注意,咱們須要用括號將它括起來,由於Javascript一般會將以 { 起始的語句解析爲一個塊。
你能夠在對象裏使用...語法建立剩餘變量:
let { a, ...passthrough } = o;
let total = passthrough.b + passthrough.c.length;
屬性重命名
你也能夠給屬性以不一樣的名字:
let { a: newName1, b: newName2 } = o;
這裏的語法開始變得混亂。 你能夠將 a: newName1 讀作 「a 做爲 newName1「。 方向是從左到右,好像你寫成了如下樣子:
let newName1 = o.a;
let newName2 = o.b;
使人困惑的是,這裏的冒號不是指示類型的。 若是你想指定它的類型, 仍然須要在其後寫上完整的模式。
let {a, b}: {a: string, b: number} = o;
默認值
默認值可讓你在屬性爲 undefined 時使用缺省值:
function keepWholeObject(wholeObject: { a: string, b?: number }) {
let { a, b = 1001 } = wholeObject;
}
如今,即便 b 爲 undefined , keepWholeObject 函數的變量 wholeObject 的屬性 a 和 b 都會有值。
函數聲明
解構也能用於函數聲明。 看如下簡單的狀況:
type C = { a: string, b?: number }
function f({ a, b }: C): void {
// ...
}
可是,一般狀況下更多的是指定默認值,解構默認值有些棘手。 首先,你須要在默認值以前設置其格式。
function f({ a, b } = { a: "", b: 0 }): void {
// ...
}
f(); // ok, default to { a: "", b: 0 }
上面的代碼是一個類型推斷的例子,將在本手冊後文介紹。
其次,你須要知道在解構屬性上給予一個默認或可選的屬性用來替換主初始化列表。 要知道 C 的定義有一個 b 可選屬性:
function f({ a, b = 0 } = { a: "" }): void {
// ...
}
f({ a: "yes" }); // ok, default b = 0
f(); // ok, default to {a: ""}, which then defaults b = 0
f({}); // error, 'a' is required if you supply an argument
要當心使用解構。 從前面的例子能夠看出,就算是最簡單的解構表達式也是難以理解的。 尤爲當存在深層嵌套解構的時候,就算這時沒有堆疊在一塊兒的重命名,默認值和類型註解,也是使人難以理解的。 解構表達式要儘可能保持小而簡單。 你本身也能夠直接使用解構將會生成的賦值表達式。
展開
展開操做符正與解構相反。 它容許你將一個數組展開爲另外一個數組,或將一個對象展開爲另外一個對象。 例如:
let first = [1, 2];
let second = [3, 4];
let bothPlus = [0, ...first, ...second, 5];
這會令bothPlus的值爲[0, 1, 2, 3, 4, 5]。 展開操做建立了first和second的一份淺拷貝。 它們不會被展開操做所改變。
你還能夠展開對象:
let defaults = { food: "spicy", price: "$$", ambiance: "noisy" };
let search = { ...defaults, food: "rich" };
search的值爲{ food: "rich", price: "$$", ambiance: "noisy" }。 對象的展開比數組的展開要複雜的多。 像數組展開同樣,它是從左至右進行處理,但結果仍爲對象。 這就意味着出如今展開對象後面的屬性會覆蓋前面的屬性。 所以,若是咱們修改上面的例子,在結尾處進行展開的話:
let defaults = { food: "spicy", price: "$$", ambiance: "noisy" };
let search = { food: "rich", ...defaults };
那麼,defaults裏的food屬性會重寫food: "rich",在這裏這並非咱們想要的結果。
對象展開還有其它一些意想不到的限制。 首先,它僅包含對象 自身的可枚舉屬性。 大致上是說當你展開一個對象實例時,你會丟失其方法:
class C {
p = 12;
m() {
}
}
let c = new C();
let clone = { ...c };
clone.p; // ok
clone.m(); // error!
接口(interface)
interface LabelledValue {
label: string;
}
function printLabel(labelledObj: LabelledValue) {
console.log(labelledObj.label);
}
let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);
可選屬性
接口裏的屬性不全都是必需的。 有些是隻在某些條件下存在,或者根本不存在。 可選屬性在應用「option bags」模式時很經常使用,即給函數傳入的參數對象中只有部分屬性賦值了。
下面是應用了「option bags」的例子:
interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): {color: string; area: number} {
let newSquare = {color: "white", area: 100};
if (config.color) {
newSquare.color = config.color;
}
if (config.width) {
newSquare.area = config.width * config.width;
}
return newSquare;
}
let mySquare = createSquare({color: "black"});
帶有可選屬性的接口與普通的接口定義差很少,只是在可選屬性名字定義的後面加一個?符號。
可選屬性的好處之一是能夠對可能存在的屬性進行預約義,好處之二是能夠捕獲引用了不存在的屬性時的錯誤。 好比,咱們故意將createSquare裏的color屬性名拼錯,就會獲得一個錯誤提示:
interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): { color: string; area: number } {
let newSquare = {color: "white", area: 100};
if (config.color) {
// Error: Property 'clor' does not exist on type 'SquareConfig'
newSquare.color = config.clor;
}
if (config.width) {
newSquare.area = config.width * config.width;
}
return newSquare;
}
let mySquare = createSquare({color: "black"});
只讀屬性
一些對象屬性只能在對象剛剛建立的時候修改其值。 你能夠在屬性名前用readonly來指定只讀屬性:
interface Point {
readonly x: number;
readonly y: number;
}
你能夠經過賦值一個對象字面量來構造一個Point。 賦值後,x和y不再能被改變了。
let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error!
TypeScript具備ReadonlyArray<T>類型,它與Array<T>類似,只是把全部可變方法去掉了,所以能夠確保數組建立後不再能被修改:
let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error!
ro.push(5); // error!
ro.length = 100; // error!
a = ro; // error!
上面代碼的最後一行,能夠看到就算把整個ReadonlyArray賦值到一個普通數組也是不能夠的。 可是你能夠用類型斷言重寫:
a = ro as number[];
readonly vs const
最簡單判斷該用readonly仍是const的方法是看要把它作爲變量使用仍是作爲一個屬性。 作爲變量使用的話用const,若作爲屬性則使用readonly。
額外的屬性檢查
咱們在第一個例子裏使用了接口,TypeScript讓咱們傳入{ size: number; label: string; }到僅指望獲得{ label: string; }的函數裏。 咱們已經學過了可選屬性,而且知道他們在「option bags」模式裏頗有用。
然而,天真地將這二者結合的話就會像在JavaScript裏那樣搬起石頭砸本身的腳。 好比,拿createSquare例子來講:
interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): { color: string; area: number } {
// ...
}
let mySquare = createSquare({ colour: "red", width: 100 });
注意傳入createSquare的參數拼寫爲colour而不是color。 在JavaScript裏,這會默默地失敗。
你可能會爭辯這個程序已經正確地類型化了,由於width屬性是兼容的,不存在color屬性,並且額外的colour屬性是無心義的。
然而,TypeScript會認爲這段代碼可能存在bug。 對象字面量會被特殊對待並且會通過額外屬性檢查,當將它們賦值給變量或做爲參數傳遞的時候。 若是一個對象字面量存在任何「目標類型」不包含的屬性時,你會獲得一個錯誤。
// error: 'colour' not expected in type 'SquareConfig'
let mySquare = createSquare({ colour: "red", width: 100 });
繞開這些檢查很是簡單。 最簡便的方法是使用類型斷言:
let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);
然而,最佳的方式是可以添加一個字符串索引簽名,前提是你可以肯定這個對象可能具備某些作爲特殊用途使用的額外屬性。 若是SquareConfig帶有上面定義的類型的color和width屬性,而且還會帶有任意數量的其它屬性,那麼咱們能夠這樣定義它:
interface SquareConfig {
color?: string;
width?: number;
}
咱們稍後會講到索引簽名,但在這咱們要表示的是SquareConfig能夠有任意數量的屬性,而且只要它們不是color和width,那麼就無所謂它們的類型是什麼。
還有最後一種跳過這些檢查的方式,這可能會讓你感到驚訝,它就是將這個對象賦值給一個另外一個變量: 由於squareOptions不會通過額外屬性檢查,因此編譯器不會報錯。
let squareOptions = { colour: "red", width: 100 };
let mySquare = createSquare(squareOptions);
要留意,在像上面同樣的簡單代碼裏,你可能不該該去繞開這些檢查。 對於包含方法和內部狀態的複雜對象字面量來說,你可能須要使用這些技巧,可是大部額外屬性檢查錯誤是真正的bug。 就是說你遇到了額外類型檢查出的錯誤,好比「option bags」,你應該去審查一下你的類型聲明。 在這裏,若是支持傳入color或colour屬性到createSquare,你應該修改SquareConfig定義來體現出這一點。
函數類型
接口可以描述JavaScript中對象擁有的各類各樣的外形。 除了描述帶有屬性的普通對象外,接口也能夠描述函數類型。
爲了使用接口表示函數類型,咱們須要給接口定義一個調用簽名。 它就像是一個只有參數列表和返回值類型的函數定義。參數列表裏的每一個參數都須要名字和類型。
interface SearchFunc {
(source: string, subString: string): boolean;
}
這樣定義後,咱們能夠像使用其它接口同樣使用這個函數類型的接口。 下例展現瞭如何建立一個函數類型的變量,並將一個同類型的函數賦值給這個變量。
let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
let result = source.search(subString);
return result > -1;
}
對於函數類型的類型檢查來講,函數的參數名不須要與接口裏定義的名字相匹配。 好比,咱們使用下面的代碼重寫上面的例子:
let mySearch: SearchFunc;
mySearch = function(src: string, sub: string): boolean {
let result = src.search(sub);
return result > -1;
}
函數的參數會逐個進行檢查,要求對應位置上的參數類型是兼容的。 若是你不想指定類型,TypeScript的類型系統會推斷出參數類型,由於函數直接賦值給了SearchFunc類型變量。 函數的返回值類型是經過其返回值推斷出來的(此例是false和true)。 若是讓這個函數返回數字或字符串,類型檢查器會警告咱們函數的返回值類型與SearchFunc接口中的定義不匹配。
let mySearch: SearchFunc;
mySearch = function(src, sub) {
let result = src.search(sub);
return result > -1;
}
可索引的類型
與使用接口描述函數類型差很少,咱們也能夠描述那些可以「經過索引獲得」的類型,好比a[10]或ageMap["daniel"]。 可索引類型具備一個索引簽名,它描述了對象索引的類型,還有相應的索引返回值類型。 讓咱們看一個例子:
interface StringArray {
}
let myArray: StringArray;
myArray = ["Bob", "Fred"];
let myStr: string = myArray[0];
上面例子裏,咱們定義了StringArray接口,它具備索引簽名。 這個索引簽名表示了當用number去索引StringArray時會獲得string類型的返回值。
共有支持兩種索引簽名:字符串和數字。 能夠同時使用兩種類型的索引,可是數字索引的返回值必須是字符串索引返回值類型的子類型。 這是由於當使用number來索引時,JavaScript會將它轉換成string而後再去索引對象。 也就是說用100(一個number)去索引等同於使用"100"(一個string)去索引,所以二者須要保持一致。
class Animal {
name: string;
}
class Dog extends Animal {
breed: string;
}
// 錯誤:使用'string'索引,有時會獲得Animal!
interface NotOkay {
[x: string]: Dog;
}
字符串索引簽名可以很好的描述dictionary模式,而且它們也會確保全部屬性與其返回值類型相匹配。 由於字符串索引聲明瞭obj.property和obj["property"]兩種形式均可以。 下面的例子裏,name的類型與字符串索引類型不匹配,因此類型檢查器給出一個錯誤提示:
interface NumberDictionary {
length: number; // 能夠,length是number類型
name: string // 錯誤,name
的類型與索引類型返回值的類型不匹配
}
最後,你能夠將索引簽名設置爲只讀,這樣就防止了給索引賦值:
interface ReadonlyStringArray {
readonly index: number: string;
}
let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; // error!
你不能設置myArray[2],由於索引簽名是隻讀的。
類類型
實現接口
與C#或Java裏接口的基本做用同樣,TypeScript也可以用它來明確的強制一個類去符合某種契約。
interface ClockInterface {
currentTime: Date;
}
class Clock implements ClockInterface {
currentTime: Date;
constructor(h: number, m: number) { }
}
你也能夠在接口中描述一個方法,在類裏實現它,如同下面的setTime方法同樣:
interface ClockInterface {
currentTime: Date;
setTime(d: Date);
}
class Clock implements ClockInterface {
currentTime: Date;
setTime(d: Date) {
this.currentTime = d;
}
constructor(h: number, m: number) { }
}
接口描述了類的公共部分,而不是公共和私有兩部分。 它不會幫你檢查類是否具備某些私有成員。
類靜態部分與實例部分的區別
當你操做類和接口的時候,你要知道類是具備兩個類型的:靜態部分的類型和實例的類型。 你會注意到,當你用構造器簽名去定義一個接口並試圖定義一個類去實現這個接口時會獲得一個錯誤:
interface ClockConstructor {
new (hour: number, minute: number);
}
class Clock implements ClockConstructor {
currentTime: Date;
constructor(h: number, m: number) { }
}
這裏由於當一個類實現了一個接口時,只對其實例部分進行類型檢查。 constructor存在於類的靜態部分,因此不在檢查的範圍內。
所以,咱們應該直接操做類的靜態部分。 看下面的例子,咱們定義了兩個接口,ClockConstructor爲構造函數所用和ClockInterface爲實例方法所用。 爲了方便咱們定義一個構造函數createClock,它用傳入的類型建立實例。
interface ClockConstructor {
new (hour: number, minute: number): ClockInterface;
}
interface ClockInterface {
tick();
}
function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {
return new ctor(hour, minute);
}
class DigitalClock implements ClockInterface {
constructor(h: number, m: number) { }
tick() {
console.log("beep beep");
}
}
class AnalogClock implements ClockInterface {
constructor(h: number, m: number) { }
tick() {
console.log("tick tock");
}
}
let digital = createClock(DigitalClock, 12, 17);
let analog = createClock(AnalogClock, 7, 32);
由於createClock的第一個參數是ClockConstructor類型,在createClock(AnalogClock, 7, 32)裏,會檢查AnalogClock是否符合構造函數簽名。
繼承接口
和類同樣,接口也能夠相互繼承。 這讓咱們可以從一個接口裏複製成員到另外一個接口裏,能夠更靈活地將接口分割到可重用的模塊裏。
interface Shape {
color: string;
}
interface Square extends Shape {
sideLength: number;
}
let square = <Square>{};
square.color = "blue";
square.sideLength = 10;
一個接口能夠繼承多個接口,建立出多個接口的合成接口。
interface Shape {
color: string;
}
interface PenStroke {
penWidth: number;
}
interface Square extends Shape, PenStroke {
sideLength: number;
}
let square = <Square>{};
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;
混合類型
先前咱們提過,接口可以描述JavaScript裏豐富的類型。 由於JavaScript其動態靈活的特色,有時你會但願一個對象能夠同時具備上面提到的多種類型。
一個例子就是,一個對象能夠同時作爲函數和對象使用,並帶有額外的屬性。
interface Counter {
(start: number): string;
interval: number;
reset(): void;
}
function getCounter(): Counter {
let counter = <Counter>function (start: number) { };
counter.interval = 123;
counter.reset = function () { };
return counter;
}
let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;
在使用JavaScript第三方庫的時候,你可能須要像上面那樣去完整地定義類型。
接口繼承類
當接口繼承了一個類類型時,它會繼承類的成員但不包括其實現。 就好像接口聲明瞭全部類中存在的成員,但並無提供具體實現同樣。 接口一樣會繼承到類的private和protected成員。 這意味着當你建立了一個接口繼承了一個擁有私有或受保護的成員的類時,這個接口類型只能被這個類或其子類所實現(implement)。
當你有一個龐大的繼承結構時這頗有用,但要指出的是你的代碼只在子類擁有特定屬性時起做用。 這個子類除了繼承至基類外與基類沒有任何關係。 例:
class Control {
private state: any;
}
interface SelectableControl extends Control {
select(): void;
}
class Button extends Control implements SelectableControl {
select() { }
}
class TextBox extends Control {
}
// Error: Property 'state' is missing in type 'Image'.
class Image implements SelectableControl {
select() { }
}
class Location {
}
在上面的例子裏,SelectableControl包含了Control的全部成員,包括私有成員state。 由於state是私有成員,因此只可以是Control的子類們才能實現SelectableControl接口。 由於只有Control的子類纔可以擁有一個聲明於Control的私有成員state,這對私有成員的兼容性是必需的。
在Control類內部,是容許經過SelectableControl的實例來訪問私有成員state的。 實際上,SelectableControl就像Control同樣,並擁有一個select方法。 Button和TextBox類是SelectableControl的子類(由於它們都繼承自Control並有select方法),但Image和Location類並非這樣的。
類
下面看一個使用類的例子:
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
let greeter = new Greeter("world");
若是你使用過C#或Java,你會對這種語法很是熟悉。 咱們聲明一個Greeter類。這個類有3個成員:一個叫作greeting的屬性,一個構造函數和一個greet方法。
你會注意到,咱們在引用任何一個類成員的時候都用了this。 它表示咱們訪問的是類的成員。
最後一行,咱們使用new構造了Greeter類的一個實例。 它會調用以前定義的構造函數,建立一個Greeter類型的新對象,並執行構造函數初始化它。
繼承
在TypeScript裏,咱們能夠使用經常使用的面向對象模式。 基於類的程序設計中一種最基本的模式是容許使用繼承來擴展示有的類。
看下面的例子:
class Animal {
move(distanceInMeters: number = 0) {
console.log(Animal moved ${distanceInMeters}m.
);
}
}
class Dog extends Animal {
bark() {
console.log('Woof! Woof!');
}
}
const dog = new Dog();
dog.bark();
dog.move(10);
dog.bark();
這個例子展現了最基本的繼承:類從基類中繼承了屬性和方法。 這裏,Dog是一個派生類,它派生自Animal基類,經過extends關鍵字。 派生類一般被稱做子類,基類一般被稱做超類。
由於Dog繼承了Animal的功能,所以咱們能夠建立一個Dog的實例,它可以bark()和move()。
下面咱們來看個更加複雜的例子。
class Animal {
name: string;
constructor(theName: string) { this.name = theName; }
move(distanceInMeters: number = 0) {
console.log(${this.name} moved ${distanceInMeters}m.
);
}
}
class Snake extends Animal {
constructor(name: string) { super(name); }
move(distanceInMeters = 5) {
console.log("Slithering...");
super.move(distanceInMeters);
}
}
class Horse extends Animal {
constructor(name: string) { super(name); }
move(distanceInMeters = 45) {
console.log("Galloping...");
super.move(distanceInMeters);
}
}
let sam = new Snake("Sammy the Python");
let tom: Animal = new Horse("Tommy the Palomino");
sam.move();
tom.move(34);
這個例子展現了一些上面沒有提到的特性。 這一次,咱們使用extends關鍵字建立了Animal的兩個子類:Horse和Snake。
與前一個例子的不一樣點是,派生類包含了一個構造函數,它必須調用super(),它會執行基類的構造函數。 並且,在構造函數裏訪問this的屬性以前,咱們必定要調用super()。 這個是TypeScript強制執行的一條重要規則。
這個例子演示瞭如何在子類裏能夠重寫父類的方法。 Snake類和Horse類都建立了move方法,它們重寫了從Animal繼承來的move方法,使得move方法根據不一樣的類而具備不一樣的功能。 注意,即便tom被聲明爲Animal類型,但由於它的值是Horse,調用tom.move(34)時,它會調用Horse裏重寫的方法:
Slithering...
Sammy the Python moved 5m.
Galloping...
Tommy the Palomino moved 34m.
公共,私有與受保護的修飾符
默認爲public
在上面的例子裏,咱們能夠自由的訪問程序裏定義的成員。 若是你對其它語言中的類比較瞭解,就會注意到咱們在以前的代碼裏並無使用public來作修飾;例如,C#要求必須明確地使用public指定成員是可見的。 在TypeScript裏,成員都默認爲public。
你也能夠明確的將一個成員標記成public。 咱們能夠用下面的方式來重寫上面的Animal類:
class Animal {
public name: string;
public constructor(theName: string) { this.name = theName; }
public move(distanceInMeters: number) {
console.log(${this.name} moved ${distanceInMeters}m.
);
}
}
理解private
當成員被標記成private時,它就不能在聲明它的類的外部訪問。好比:
class Animal {
private name: string;
constructor(theName: string) { this.name = theName; }
}
new Animal("Cat").name; // 錯誤: 'name' 是私有的.
TypeScript使用的是結構性類型系統。 當咱們比較兩種不一樣的類型時,並不在意它們從何處而來,若是全部成員的類型都是兼容的,咱們就認爲它們的類型是兼容的。
然而,當咱們比較帶有private或protected成員的類型的時候,狀況就不一樣了。 若是其中一個類型裏包含一個private成員,那麼只有當另一個類型中也存在這樣一個private成員, 而且它們都是來自同一處聲明時,咱們才認爲這兩個類型是兼容的。 對於protected成員也使用這個規則。
下面來看一個例子,更好地說明了這一點:
class Animal {
private name: string;
constructor(theName: string) { this.name = theName; }
}
class Rhino extends Animal {
constructor() { super("Rhino"); }
}
class Employee {
private name: string;
constructor(theName: string) { this.name = theName; }
}
let animal = new Animal("Goat");
let rhino = new Rhino();
let employee = new Employee("Bob");
animal = rhino;
animal = employee; // 錯誤: Animal 與 Employee 不兼容.
這個例子中有Animal和Rhino兩個類,Rhino是Animal類的子類。 還有一個Employee類,其類型看上去與Animal是相同的。 咱們建立了幾個這些類的實例,並相互賦值來看看會發生什麼。 由於Animal和Rhino共享了來自Animal裏的私有成員定義private name: string,所以它們是兼容的。 然而Employee卻不是這樣。當把Employee賦值給Animal的時候,獲得一個錯誤,說它們的類型不兼容。 儘管Employee裏也有一個私有成員name,但它明顯不是Animal裏面定義的那個。
理解protected
protected修飾符與private修飾符的行爲很類似,但有一點不一樣,protected成員在派生類中仍然能夠訪問。例如:
class Person {
protected name: string;
constructor(name: string) { this.name = name; }
}
class Employee extends Person {
private department: string;
constructor(name: string, department: string) { super(name) this.department = department; } public getElevatorPitch() { return `Hello, my name is ${this.name} and I work in ${this.department}.`; }
}
let howard = new Employee("Howard", "Sales");
console.log(howard.getElevatorPitch());
console.log(howard.name); // 錯誤
注意,咱們不能在Person類外使用name,可是咱們仍然能夠經過Employee類的實例方法訪問,由於Employee是由Person派生而來的。
構造函數也能夠被標記成protected。 這意味着這個類不能在包含它的類外被實例化,可是能被繼承。好比,
class Person {
protected name: string;
protected constructor(theName: string) { this.name = theName; }
}
// Employee 可以繼承 Person
class Employee extends Person {
private department: string;
constructor(name: string, department: string) { super(name); this.department = department; } public getElevatorPitch() { return `Hello, my name is ${this.name} and I work in ${this.department}.`; }
}
let howard = new Employee("Howard", "Sales");
let john = new Person("John"); // 錯誤: 'Person' 的構造函數是被保護的.
readonly修飾符
你能夠使用readonly關鍵字將屬性設置爲只讀的。 只讀屬性必須在聲明時或構造函數裏被初始化。
class Octopus {
readonly name: string;
readonly numberOfLegs: number = 8;
constructor (theName: string) {
this.name = theName;
}
}
let dad = new Octopus("Man with the 8 strong legs");
dad.name = "Man with the 3-piece suit"; // 錯誤! name 是隻讀的.
參數屬性
在上面的例子中,咱們不得不定義一個受保護的成員name和一個構造函數參數theName在Person類裏,而且馬上給name和theName賦值。 這種狀況常常會遇到。參數屬性能夠方便地讓咱們在一個地方定義並初始化一個成員。 下面的例子是對以前Animal類的修改版,使用了參數屬性:
class Animal {
constructor(private name: string) { }
move(distanceInMeters: number) {
console.log(${this.name} moved ${distanceInMeters}m.
);
}
}
注意看咱們是如何捨棄了theName,僅在構造函數裏使用private name: string參數來建立和初始化name成員。 咱們把聲明和賦值合併至一處。
參數屬性經過給構造函數參數添加一個訪問限定符來聲明。 使用private限定一個參數屬性會聲明並初始化一個私有成員;對於public和protected來講也是同樣。
存取器
TypeScript支持經過getters/setters來截取對對象成員的訪問。 它能幫助你有效的控制對對象成員的訪問。
下面來看如何把一個簡單的類改寫成使用get和set。 首先,咱們從一個沒有使用存取器的例子開始。
class Employee {
fullName: string;
}
let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
console.log(employee.fullName);
}
咱們能夠隨意的設置fullName,這是很是方便的,可是這也可能會帶來麻煩。
下面這個版本里,咱們先檢查用戶密碼是否正確,而後再容許其修改員工信息。 咱們把對fullName的直接訪問改爲了能夠檢查密碼的set方法。 咱們也加了一個get方法,讓上面的例子仍然能夠工做。
let passcode = "secret passcode";
class Employee {
private _fullName: string;
get fullName(): string { return this._fullName; } set fullName(newName: string) { if (passcode && passcode == "secret passcode") { this._fullName = newName; } else { console.log("Error: Unauthorized update of employee!"); } }
}
let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
alert(employee.fullName);
}
咱們能夠修改一下密碼,來驗證一下存取器是不是工做的。當密碼不對時,會提示咱們沒有權限去修改員工。
對於存取器有下面幾點須要注意的:
首先,存取器要求你將編譯器設置爲輸出ECMAScript 5或更高。 不支持降級到ECMAScript 3。 其次,只帶有get不帶有set的存取器自動被推斷爲readonly。 這在從代碼生成.d.ts文件時是有幫助的,由於利用這個屬性的用戶會看到不容許夠改變它的值。
靜態屬性
到目前爲止,咱們只討論了類的實例成員,那些僅當類被實例化的時候纔會被初始化的屬性。 咱們也能夠建立類的靜態成員,這些屬性存在於類自己上面而不是類的實例上。 在這個例子裏,咱們使用static定義origin,由於它是全部網格都會用到的屬性。 每一個實例想要訪問這個屬性的時候,都要在origin前面加上類名。 如同在實例屬性上使用this.前綴來訪問屬性同樣,這裏咱們使用Grid.來訪問靜態屬性。
class Grid {
static origin = {x: 0, y: 0};
calculateDistanceFromOrigin(point: {x: number; y: number;}) {
let xDist = (point.x - Grid.origin.x);
let yDist = (point.y - Grid.origin.y);
return Math.sqrt(xDist xDist + yDist yDist) / this.scale;
}
constructor (public scale: number) { }
}
let grid1 = new Grid(1.0); // 1x scale
let grid2 = new Grid(5.0); // 5x scale
console.log(grid1.calculateDistanceFromOrigin({x: 10, y: 10}));
console.log(grid2.calculateDistanceFromOrigin({x: 10, y: 10}));
抽象類
抽象類作爲其它派生類的基類使用。 它們通常不會直接被實例化。 不一樣於接口,抽象類能夠包含成員的實現細節。 abstract關鍵字是用於定義抽象類和在抽象類內部定義抽象方法。
abstract class Animal {
abstract makeSound(): void;
move(): void {
console.log('roaming the earch...');
}
}
抽象類中的抽象方法不包含具體實現而且必須在派生類中實現。 抽象方法的語法與接口方法類似。 二者都是定義方法簽名但不包含方法體。 然而,抽象方法必須包含abstract關鍵字而且能夠包含訪問修飾符。
abstract class Department {
constructor(public name: string) { } printName(): void { console.log('Department name: ' + this.name); } abstract printMeeting(): void; // 必須在派生類中實現
}
class AccountingDepartment extends Department {
constructor() { super('Accounting and Auditing'); // 在派生類的構造函數中必須調用 super() } printMeeting(): void { console.log('The Accounting Department meets each Monday at 10am.'); } generateReports(): void { console.log('Generating accounting reports...'); }
}
let department: Department; // 容許建立一個對抽象類型的引用
department = new Department(); // 錯誤: 不能建立一個抽象類的實例
department = new AccountingDepartment(); // 容許對一個抽象子類進行實例化和賦值
department.printName();
department.printMeeting();
department.generateReports(); // 錯誤: 方法在聲明的抽象類中不存在
高級技巧
構造函數
當你在TypeScript裏聲明瞭一個類的時候,實際上同時聲明瞭不少東西。 首先就是類的實例的類型。
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
let greeter: Greeter;
greeter = new Greeter("world");
console.log(greeter.greet());
這裏,咱們寫了let greeter: Greeter,意思是Greeter類的實例的類型是Greeter。 這對於用過其它面嚮對象語言的程序員來說已是老習慣了。
咱們也建立了一個叫作構造函數的值。 這個函數會在咱們使用new建立類實例的時候被調用。 下面咱們來看看,上面的代碼被編譯成JavaScript後是什麼樣子的:
let Greeter = (function () {
function Greeter(message) {
this.greeting = message;
}
Greeter.prototype.greet = function () {
return "Hello, " + this.greeting;
};
return Greeter;
})();
let greeter;
greeter = new Greeter("world");
console.log(greeter.greet());
上面的代碼裏,let Greeter將被賦值爲構造函數。 當咱們調用new並執行了這個函數後,便會獲得一個類的實例。 這個構造函數也包含了類的全部靜態屬性。 換個角度說,咱們能夠認爲類具備實例部分與靜態部分這兩個部分。
讓咱們稍微改寫一下這個例子,看看它們以前的區別:
class Greeter {
static standardGreeting = "Hello, there";
greeting: string;
greet() {
if (this.greeting) {
return "Hello, " + this.greeting;
}
else {
return Greeter.standardGreeting;
}
}
}
let greeter1: Greeter;
greeter1 = new Greeter();
console.log(greeter1.greet());
let greeterMaker: typeof Greeter = Greeter;
greeterMaker.standardGreeting = "Hey there!";
let greeter2: Greeter = new greeterMaker();
console.log(greeter2.greet());
這個例子裏,greeter1與以前看到的同樣。 咱們實例化Greeter類,並使用這個對象。 與咱們以前看到的同樣。
再以後,咱們直接使用類。 咱們建立了一個叫作greeterMaker的變量。 這個變量保存了這個類或者說保存了類構造函數。 而後咱們使用typeof Greeter,意思是取Greeter類的類型,而不是實例的類型。 或者更確切的說,」告訴我Greeter標識符的類型」,也就是構造函數的類型。 這個類型包含了類的全部靜態成員和構造函數。 以後,就和前面同樣,咱們在greeterMaker上使用new,建立Greeter的實例。
把類當作接口使用
如上一節裏所講的,類定義會建立兩個東西:類的實例類型和一個構造函數。 由於類能夠建立出類型,因此你可以在容許使用接口的地方使用類。
class Point {
x: number;
y: number;
}
interface Point3d extends Point {
z: number;
}
let point3d: Point3d = {x: 1, y: 2, z: 3};
函數(function)
介紹
函數是JavaScript應用程序的基礎。 它幫助你實現抽象層,模擬類,信息隱藏和模塊。 在TypeScript裏,雖然已經支持類,命名空間和模塊,但函數仍然是主要的定義行爲的地方。 TypeScript爲JavaScript函數添加了額外的功能,讓咱們能夠更容易地使用。
函數
和JavaScript同樣,TypeScript函數能夠建立有名字的函數和匿名函數。 你能夠隨意選擇適合應用程序的方式,不管是定義一系列API函數仍是隻使用一次的函數。
經過下面的例子能夠迅速回想起這兩種JavaScript中的函數:
// Named function
function add(x, y) {
return x + y;
}
// Anonymous function
let myAdd = function(x, y) { return x + y; };
在JavaScript裏,函數能夠使用函數體外部的變量。 當函數這麼作時,咱們說它‘捕獲’了這些變量。 至於爲何能夠這樣作以及其中的利弊超出了本文的範圍,可是深入理解這個機制對學習JavaScript和TypeScript會頗有幫助。
let z = 100;
function addToZ(x, y) {
return x + y + z;
}
函數類型
爲函數定義類型
讓咱們爲上面那個函數添加類型:
function add(x: number, y: number): number {
return x + y;
}
let myAdd = function(x: number, y: number): number { return x + y; };
咱們能夠給每一個參數添加類型以後再爲函數自己添加返回值類型。 TypeScript可以根據返回語句自動推斷出返回值類型,所以咱們一般省略它。
書寫完整函數類型
如今咱們已經爲函數指定了類型,下面讓咱們寫出函數的完整類型。
let myAdd: (x:number, y:number) => number =
function(x: number, y: number): number { return x + y; };
函數類型包含兩部分:參數類型和返回值類型。 當寫出完整函數類型的時候,這兩部分都是須要的。 咱們以參數列表的形式寫出參數類型,爲每一個參數指定一個名字和類型。 這個名字只是爲了增長可讀性。 咱們也能夠這麼寫:
let myAdd: (baseValue: number, increment: number) => number =
function(x: number, y: number): number { return x + y; };
只要參數類型是匹配的,那麼就認爲它是有效的函數類型,而不在意參數名是否正確。
第二部分是返回值類型。 對於返回值,咱們在函數和返回值類型以前使用(=>)符號,使之清晰明瞭。 如以前提到的,返回值類型是函數類型的必要部分,若是函數沒有返回任何值,你也必須指定返回值類型爲void而不能留空。
函數的類型只是由參數類型和返回值組成的。 函數中使用的捕獲變量不會體如今類型裏。 實際上,這些變量是函數的隱藏狀態並非組成API的一部分。
推斷類型
嘗試這個例子的時候,你會發現若是你在賦值語句的一邊指定了類型可是另外一邊沒有類型的話,TypeScript編譯器會自動識別出類型:
// myAdd has the full function type
let myAdd = function(x: number, y: number): number { return x + y; };
// The parameters x
and y
have the type number
let myAdd: (baseValue: number, increment: number) => number =
function(x, y) { return x + y; };
這叫作「按上下文歸類」,是類型推論的一種。 它幫助咱們更好地爲程序指定類型。
可選參數和默認參數
TypeScript裏的每一個函數參數都是必須的。 這不是指不能傳遞null或undefined做爲參數,而是說編譯器檢查用戶是否爲每一個參數都傳入了值。 編譯器還會假設只有這些參數會被傳遞進函數。 簡短地說,傳遞給一個函數的參數個數必須與函數指望的參數個數一致。
function buildName(firstName: string, lastName: string) {
return firstName + " " + lastName;
}
let result1 = buildName("Bob"); // error, too few parameters
let result2 = buildName("Bob", "Adams", "Sr."); // error, too many parameters
let result3 = buildName("Bob", "Adams"); // ah, just right
JavaScript裏,每一個參數都是可選的,可傳可不傳。 沒傳參的時候,它的值就是undefined。 在TypeScript裏咱們能夠在參數名旁使用?實現可選參數的功能。 好比,咱們想讓last name是可選的:
function buildName(firstName: string, lastName?: string) {
if (lastName)
return firstName + " " + lastName;
else
return firstName;
}
let result1 = buildName("Bob"); // works correctly now
let result2 = buildName("Bob", "Adams", "Sr."); // error, too many parameters
let result3 = buildName("Bob", "Adams"); // ah, just right
可選參數必須跟在必須參數後面。 若是上例咱們想讓first name是可選的,那麼就必須調整它們的位置,把first name放在後面。
在TypeScript裏,咱們也能夠爲參數提供一個默認值當用戶沒有傳遞這個參數或傳遞的值是undefined時。 它們叫作有默認初始化值的參數。 讓咱們修改上例,把last name的默認值設置爲"Smith"。
function buildName(firstName: string, lastName = "Smith") {
return firstName + " " + lastName;
}
let result1 = buildName("Bob"); // works correctly now, returns "Bob Smith"
let result2 = buildName("Bob", undefined); // still works, also returns "Bob Smith"
let result3 = buildName("Bob", "Adams", "Sr."); // error, too many parameters
let result4 = buildName("Bob", "Adams"); // ah, just right
在全部必須參數後面的帶默認初始化的參數都是可選的,與可選參數同樣,在調用函數的時候能夠省略。 也就是說可選參數與末尾的默認參數共享參數類型。
function buildName(firstName: string, lastName?: string) {
// ...
}
和
function buildName(firstName: string, lastName = "Smith") {
// ...
}
共享一樣的類型(firstName: string, lastName?: string) => string。 默認參數的默認值消失了,只保留了它是一個可選參數的信息。
與普通可選參數不一樣的是,帶默認值的參數不須要放在必須參數的後面。 若是帶默認值的參數出如今必須參數前面,用戶必須明確的傳入undefined值來得到默認值。 例如,咱們重寫最後一個例子,讓firstName是帶默認值的參數:
function buildName(firstName = "Will", lastName: string) {
return firstName + " " + lastName;
}
let result1 = buildName("Bob"); // error, too few parameters
let result2 = buildName("Bob", "Adams", "Sr."); // error, too many parameters
let result3 = buildName("Bob", "Adams"); // okay and returns "Bob Adams"
let result4 = buildName(undefined, "Adams"); // okay and returns "Will Adams"
剩餘參數
必要參數,默認參數和可選參數有個共同點:它們表示某一個參數。 有時,你想同時操做多個參數,或者你並不知道會有多少參數傳遞進來。 在JavaScript裏,你能夠使用arguments來訪問全部傳入的參數。
在TypeScript裏,你能夠把全部參數收集到一個變量裏:
function buildName(firstName: string, ...restOfName: string[]) {
return firstName + " " + restOfName.join(" ");
}
let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");
剩餘參數會被當作個數不限的可選參數。 能夠一個都沒有,一樣也能夠有任意個。 編譯器建立參數數組,名字是你在省略號(...)後面給定的名字,你能夠在函數體內使用這個數組。
這個省略號也會在帶有剩餘參數的函數類型定義上使用到:
function buildName(firstName: string, ...restOfName: string[]) {
return firstName + " " + restOfName.join(" ");
}
let buildNameFun: (fname: string, ...rest: string[]) => string = buildName;
this
學習如何在JavaScript里正確使用this就比如一場成年禮。 因爲TypeScript是JavaScript的超集,TypeScript程序員也須要弄清this工做機制而且當有bug的時候可以找出錯誤所在。 幸運的是,TypeScript能通知你錯誤地使用了this的地方。 若是你想了解JavaScript裏的this是如何工做的,那麼首先閱讀Yehuda Katz寫的Understanding JavaScript Function Invocation and 「this」。 Yehuda的文章詳細的闡述了this的內部工做原理,所以咱們這裏只作簡單介紹。
this和箭頭函數
JavaScript裏,this的值在函數被調用的時候纔會指定。 這是個既強大又靈活的特色,可是你須要花點時間弄清楚函數調用的上下文是什麼。 但衆所周知,這不是一件很簡單的事,尤爲是在返回一個函數或將函數當作參數傳遞的時候。
下面看一個例子:
let deck = {
suits: ["hearts", "spades", "clubs", "diamonds"],
cards: Array(52),
createCardPicker: function() {
return function() {
let pickedCard = Math.floor(Math.random() * 52);
let pickedSuit = Math.floor(pickedCard / 13);
return {suit: this.suits[pickedSuit], card: pickedCard % 13}; } }
}
let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();
alert("card: " + pickedCard.card + " of " + pickedCard.suit);
能夠看到createCardPicker是個函數,而且它又返回了一個函數。 若是咱們嘗試運行這個程序,會發現它並無彈出對話框而是報錯了。 由於createCardPicker返回的函數裏的this被設置成了window而不是deck對象。 由於咱們只是獨立的調用了cardPicker()。 頂級的非方法式調用會將this視爲window。 (注意:在嚴格模式下,this爲undefined而不是window)。
爲了解決這個問題,咱們能夠在函數被返回時就綁好正確的this。 這樣的話,不管以後怎麼使用它,都會引用綁定的‘deck’對象。 咱們須要改變函數表達式來使用ECMAScript 6箭頭語法。 箭頭函數能保存函數建立時的this值,而不是調用時的值:
let deck = {
suits: ["hearts", "spades", "clubs", "diamonds"],
cards: Array(52),
createCardPicker: function() {
// NOTE: the line below is now an arrow function, allowing us to capture 'this' right here
return () => {
let pickedCard = Math.floor(Math.random() * 52);
let pickedSuit = Math.floor(pickedCard / 13);
return {suit: this.suits[pickedSuit], card: pickedCard % 13}; } }
}
let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();
alert("card: " + pickedCard.card + " of " + pickedCard.suit);
更好事情是,TypeScript會警告你犯了一個錯誤,若是你給編譯器設置了--noImplicitThis標記。 它會指出this.suits[pickedSuit]裏的this的類型爲any。
this參數
不幸的是,this.suits[pickedSuit]的類型依舊爲any。 這是由於this來自對象字面量裏的函數表達式。 修改的方法是,提供一個顯式的this參數。 this參數是個假的參數,它出如今參數列表的最前面:
function f(this: void) {
// make sure this
is unusable in this standalone function
}
讓咱們往例子裏添加一些接口,Card 和 Deck,讓類型重用可以變得清晰簡單些:
interface Card {
suit: string;
card: number;
}
interface Deck {
suits: string[];
cards: number[];
createCardPicker(this: Deck): () => Card;
}
let deck: Deck = {
suits: ["hearts", "spades", "clubs", "diamonds"],
cards: Array(52),
// NOTE: The function now explicitly specifies that its callee must be of type Deck
createCardPicker: function(this: Deck) {
return () => {
let pickedCard = Math.floor(Math.random() * 52);
let pickedSuit = Math.floor(pickedCard / 13);
return {suit: this.suits[pickedSuit], card: pickedCard % 13}; } }
}
let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();
alert("card: " + pickedCard.card + " of " + pickedCard.suit);
如今TypeScript知道createCardPicker指望在某個Deck對象上調用。 也就是說this是Deck類型的,而非any,所以--noImplicitThis不會報錯了。
回調函數裏的this參數
當你將一個函數傳遞到某個庫函數裏在稍後被調用時,你可能也見到過回調函數裏的this會報錯。 由於當回調函數被調用時,它會被當成一個普通函數調用,this將爲undefined。 稍作改動,你就能夠經過this參數來避免錯誤。 首先,庫函數的做者要指定this的類型:
interface UIElement {
addClickListener(onclick: (this: void, e: Event) => void): void;
}
this: void意味着addClickListener指望onclick是一個函數且它不須要一個this類型。 而後,爲調用代碼裏的this添加類型註解:
class Handler {
info: string;
onClickBad(this: Handler, e: Event) {
// oops, used this here. using this callback would crash at runtime
this.info = e.message;
}
}
let h = new Handler();
uiElement.addClickListener(h.onClickBad); // error!
指定了this類型後,你顯式聲明onClickBad必須在Handler的實例上調用。 而後TypeScript會檢測到addClickListener要求函數帶有this: void。 改變this類型來修復這個錯誤:
class Handler {
info: string;
onClickGood(this: void, e: Event) {
// can't use this here because it's of type void!
console.log('clicked!');
}
}
let h = new Handler();
uiElement.addClickListener(h.onClickGood);
由於onClickGood指定了this類型爲void,所以傳遞addClickListener是合法的。 固然了,這也意味着不能使用this.info. 若是你二者都想要,你不得不使用箭頭函數了:
class Handler {
info: string;
onClickGood = (e: Event) => { this.info = e.message }
}
這是可行的由於箭頭函數不會捕獲this,因此你老是能夠把它們傳給指望this: void的函數。 缺點是每一個Handler對象都會建立一個箭頭函數。 另外一方面,方法只會被建立一次,添加到Handler的原型鏈上。 它們在不一樣Handler對象間是共享的。
重載
JavaScript自己是個動態語言。 JavaScript裏函數根據傳入不一樣的參數而返回不一樣類型的數據是很常見的。
let suits = ["hearts", "spades", "clubs", "diamonds"];
function pickCard(x): any {
// Check to see if we're working with an object/array
// if so, they gave us the deck and we'll pick the card
if (typeof x == "object") {
let pickedCard = Math.floor(Math.random() * x.length);
return pickedCard;
}
// Otherwise just let them pick the card
else if (typeof x == "number") {
let pickedSuit = Math.floor(x / 13);
return { suit: suits[pickedSuit], card: x % 13 };
}
}
let myDeck = [{ suit: "diamonds", card: 2 }, { suit: "spades", card: 10 }, { suit: "hearts", card: 4 }];
let pickedCard1 = myDeck[pickCard(myDeck)];
alert("card: " + pickedCard1.card + " of " + pickedCard1.suit);
let pickedCard2 = pickCard(15);
alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);
pickCard方法根據傳入參數的不一樣會返回兩種不一樣的類型。 若是傳入的是表明紙牌的對象,函數做用是從中抓一張牌。 若是用戶想抓牌,咱們告訴他抓到了什麼牌。 可是這怎麼在類型系統裏表示呢。
方法是爲同一個函數提供多個函數類型定義來進行函數重載。 編譯器會根據這個列表去處理函數的調用。 下面咱們來重載pickCard函數。
let suits = ["hearts", "spades", "clubs", "diamonds"];
function pickCard(x: {suit: string; card: number; }[]): number;
function pickCard(x: number): {suit: string; card: number; };
function pickCard(x): any {
// Check to see if we're working with an object/array
// if so, they gave us the deck and we'll pick the card
if (typeof x == "object") {
let pickedCard = Math.floor(Math.random() * x.length);
return pickedCard;
}
// Otherwise just let them pick the card
else if (typeof x == "number") {
let pickedSuit = Math.floor(x / 13);
return { suit: suits[pickedSuit], card: x % 13 };
}
}
let myDeck = [{ suit: "diamonds", card: 2 }, { suit: "spades", card: 10 }, { suit: "hearts", card: 4 }];
let pickedCard1 = myDeck[pickCard(myDeck)];
alert("card: " + pickedCard1.card + " of " + pickedCard1.suit);
let pickedCard2 = pickCard(15);
alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);
這樣改變後,重載的pickCard函數在調用的時候會進行正確的類型檢查。
爲了讓編譯器可以選擇正確的檢查類型,它與JavaScript裏的處理流程類似。 它查找重載列表,嘗試使用第一個重載定義。 若是匹配的話就使用這個。 所以,在定義重載的時候,必定要把最精確的定義放在最前面。
注意,function pickCard(x): any並非重載列表的一部分,所以這裏只有兩個重載:一個是接收對象另外一個接收數字。 以其它參數調用pickCard會產生錯誤
泛型(generic)
組件不只可以支持當前的數據類型,同時也能支持將來的數據類型,這在建立大型系統時爲你提供了十分靈活的功能。
在像C#和Java這樣的語言中,能夠使用泛型來建立可重用的組件,一個組件能夠支持多種類型的數據。 這樣用戶就能夠以本身的數據類型來使用組件。
泛型之Hello World
下面來建立第一個使用泛型的例子:identity函數。 這個函數會返回任何傳入它的值。 你能夠把這個函數當成是echo命令。
不用泛型的話,這個函數多是下面這樣:
function identity(arg: number): number {
return arg;
}
或者,咱們使用any類型來定義函數:
function identity(arg: any): any {
return arg;
}
使用any類型會致使這個函數能夠接收任何類型的arg參數,這樣就丟失了一些信息:傳入的類型與返回的類型應該是相同的。 若是咱們傳入一個數字,咱們只知道任何類型的值都有可能被返回。
所以,咱們須要一種方法使返回值的類型與傳入參數的類型是相同的。 這裏,咱們使用了類型變量,它是一種特殊的變量,只用於表示類型而不是值。
function identity<T>(arg: T): T {
return arg;
}
咱們給identity添加了類型變量T。 T幫助咱們捕獲用戶傳入的類型(好比:number),以後咱們就能夠使用這個類型。 以後咱們再次使用了T當作返回值類型。如今咱們能夠知道參數類型與返回值類型是相同的了。 這容許咱們跟蹤函數裏使用的類型的信息。
咱們把這個版本的identity函數叫作泛型,由於它能夠適用於多個類型。 不一樣於使用any,它不會丟失信息,像第一個例子那像保持準確性,傳入數值類型並返回數值類型。
咱們定義了泛型函數後,能夠用兩種方法使用。 第一種是,傳入全部的參數,包含類型參數:
let output = identity<string>("myString"); // type of output will be 'string'
這裏咱們明確的指定了T是string類型,並作爲一個參數傳給函數,使用了<>括起來而不是()。
第二種方法更廣泛。利用了類型推論 – 即編譯器會根據傳入的參數自動地幫助咱們肯定T的類型:
let output = identity("myString"); // type of output will be 'string'
注意咱們不必使用尖括號(<>)來明確地傳入類型;編譯器能夠查看myString的值,而後把T設置爲它的類型。 類型推論幫助咱們保持代碼精簡和高可讀性。若是編譯器不可以自動地推斷出類型的話,只能像上面那樣明確的傳入T的類型,在一些複雜的狀況下,這是可能出現的。
使用泛型變量
使用泛型建立像identity這樣的泛型函數時,編譯器要求你在函數體必須正確的使用這個通用的類型。 換句話說,你必須把這些參數當作是任意或全部類型。
看下以前identity例子:
function identity<T>(arg: T): T {
return arg;
}
若是咱們想同時打印出arg的長度。 咱們極可能會這樣作:
function loggingIdentity<T>(arg: T): T {
console.log(arg.length); // Error: T doesn't have .length
return arg;
}
若是這麼作,編譯器會報錯說咱們使用了arg的.length屬性,可是沒有地方指明arg具備這個屬性。 記住,這些類型變量表明的是任意類型,因此使用這個函數的人可能傳入的是個數字,而數字是沒有.length屬性的。
如今假設咱們想操做T類型的數組而不直接是T。因爲咱們操做的是數組,因此.length屬性是應該存在的。 咱們能夠像建立其它數組同樣建立這個數組:
function loggingIdentity<T>(arg: T[]): T[] {
console.log(arg.length); // Array has a .length, so no more error
return arg;
}
你能夠這樣理解loggingIdentity的類型:泛型函數loggingIdentity,接收類型參數T和參數arg,它是個元素類型是T的數組,並返回元素類型是T的數組。 若是咱們傳入數字數組,將返回一個數字數組,由於此時T的的類型爲number。 這可讓咱們把泛型變量T當作類型的一部分使用,而不是整個類型,增長了靈活性。
咱們也能夠這樣實現上面的例子:
function loggingIdentity<T>(arg: Array<T>): Array<T> {
console.log(arg.length); // Array has a .length, so no more error
return arg;
}
使用過其它語言的話,你可能對這種語法已經很熟悉了。 在下一節,會介紹如何建立自定義泛型像Array<T>同樣。
泛型類型咱們建立了identity通用函數,能夠適用於不一樣的類型。 在這節,咱們研究一下函數自己的類型,以及如何建立泛型接口。
泛型函數的類型與非泛型函數的類型沒什麼不一樣,只是有一個類型參數在最前面,像函數聲明同樣:
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: <T>(arg: T) => T = identity;
咱們也能夠使用不一樣的泛型參數名,只要在數量上和使用方式上能對應上就能夠。
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: <U>(arg: U) => U = identity;
咱們還能夠使用帶有調用簽名的對象字面量來定義泛型函數:
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: {<T>(arg: T): T} = identity;
這引導咱們去寫第一個泛型接口了。 咱們把上面例子裏的對象字面量拿出來作爲一個接口:
interface GenericIdentityFn {
<T>(arg: T): T;
}
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn = identity;
一個類似的例子,咱們可能想把泛型參數看成整個接口的一個參數。 這樣咱們就能清楚的知道使用的具體是哪一個泛型類型(好比:Dictionary<string>而不僅是Dictionary)。 這樣接口裏的其它成員也能知道這個參數的類型了。
interface GenericIdentityFn<T> {
(arg: T): T;
}
function identity<T>(arg: T): T {
return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;
注意,咱們的示例作了少量改動。 再也不描述泛型函數,而是把非泛型函數簽名做爲泛型類型一部分。 當咱們使用GenericIdentityFn的時候,還得傳入一個類型參數來指定泛型類型(這裏是:number),鎖定了以後代碼裏使用的類型。 對於描述哪部分類型屬於泛型部分來講,理解什麼時候把參數放在調用簽名裏和什麼時候放在接口上是頗有幫助的。
除了泛型接口,咱們還能夠建立泛型類。 注意,沒法建立泛型枚舉和泛型命名空間。
泛型類
泛型類看上去與泛型接口差很少。 泛型類使用(<>)括起泛型類型,跟在類名後面。
class GenericNumber<T> {
zeroValue: T;
add: (x: T, y: T) => T;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };
GenericNumber類的使用是十分直觀的,而且你可能已經注意到了,沒有什麼去限制它只能使用number類型。 也能夠使用字符串或其它更復雜的類型。
let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function(x, y) { return x + y; };
alert(stringNumeric.add(stringNumeric.zeroValue, "test"));
與接口同樣,直接把泛型類型放在類後面,能夠幫助咱們確認類的全部屬性都在使用相同的類型。
咱們在類那節說過,類有兩部分:靜態部分和實例部分。 泛型類指的是實例部分的類型,因此類的靜態屬性不能使用這個泛型類型。
泛型約束
你應該會記得以前的一個例子,咱們有時候想操做某類型的一組值,而且咱們知道這組值具備什麼樣的屬性。 在loggingIdentity例子中,咱們想訪問arg的length屬性,可是編譯器並不能證實每種類型都有length屬性,因此就報錯了。
function loggingIdentity<T>(arg: T): T {
console.log(arg.length); // Error: T doesn't have .length
return arg;
}
相比於操做any全部類型,咱們想要限制函數去處理任意帶有.length屬性的全部類型。 只要傳入的類型有這個屬性,咱們就容許,就是說至少包含這一屬性。 爲此,咱們須要列出對於T的約束要求。
爲此,咱們定義一個接口來描述約束條件。 建立一個包含.length屬性的接口,使用這個接口和extends關鍵字還實現約束:
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length); // Now we know it has a .length property, so no more error
return arg;
}
如今這個泛型函數被定義了約束,所以它再也不是適用於任意類型:
loggingIdentity(3); // Error, number doesn't have a .length property
咱們須要傳入符合約束類型的值,必須包含必須的屬性:
loggingIdentity({length: 10, value: 3});
在泛型約束中使用類型參數
你能夠聲明一個類型參數,且它被另外一個類型參數所約束。 好比,如今咱們想要用屬性名從對象裏獲取這個屬性。 而且咱們想要確保這個屬性存在於對象obj上,所以咱們須要在這兩個類型之間使用約束。
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a"); // okay
getProperty(x, "m"); // error: Argument of type 'm' isn't assignable to 'a' | 'b' | 'c' | 'd'.
在泛型裏使用類類型
在TypeScript使用泛型建立工廠函數時,須要引用構造函數的類類型。好比,
function create<T>(c: {new(): T; }): T {
return new c();
}
一個更高級的例子,使用原型屬性推斷並約束構造函數與類實例的關係。
class BeeKeeper {
hasMask: boolean;
}
class ZooKeeper {
nametag: string;
}
class Animal {
numLegs: number;
}
class Bee extends Animal {
keeper: BeeKeeper;
}
class Lion extends Animal {
keeper: ZooKeeper;
}
function createInstance<A extends Animal>(c: new () => A): A {
return new c();
}
createInstance(Lion).keeper.nametag; // typechecks!
createInstance(Bee).keeper.hasMask; // typechecks!
枚舉
使用枚舉咱們能夠定義一些有名字的數字常量。 枚舉經過enum關鍵字來定義。
enum Direction {
Up = 1,
Down,
Left,
Right
}
一個枚舉類型能夠包含零個或多個枚舉成員。 枚舉成員具備一個數字值,它能夠是常數或是計算得出的值 當知足以下條件時,枚舉成員被看成是常數:
不具備初始化函數而且以前的枚舉成員是常數。 在這種狀況下,當前枚舉成員的值爲上一個枚舉成員的值加1。 但第一個枚舉元素是個例外。 若是它沒有初始化方法,那麼它的初始值爲0。
枚舉成員使用常數枚舉表達式初始化。 常數枚舉表達式是TypeScript表達式的子集,它能夠在編譯階段求值。 當一個表達式知足下面條件之一時,它就是一個常數枚舉表達式:
數字字面量
引用以前定義的常數枚舉成員(能夠是在不一樣的枚舉類型中定義的) 若是這個成員是在同一個枚舉類型中定義的,能夠使用非限定名來引用。
帶括號的常數枚舉表達式
+, -, ~ 一元運算符應用於常數枚舉表達式
+, -, , /, %, <<, >>, >>>, &, |, ^ 二元運算符,常數枚舉表達式作爲其一個操做對象。 若常數枚舉表達式求值後爲NaN或Infinity,則會在編譯階段報錯。
全部其它狀況的枚舉成員被看成是須要計算得出的值。
enum FileAccess {
// constant members
None,
Read = 1 << 1,
Write = 1 << 2,
ReadWrite = Read | Write,
// computed member
G = "123".length
}
枚舉是在運行時真正存在的一個對象。 其中一個緣由是由於這樣能夠從枚舉值到枚舉名進行反向映射。
enum Enum {
A
}
let a = Enum.A;
let nameOfA = Enum[a]; // "A"
編譯成:
var Enum;
(function (Enum) {
Enum[Enum["A"] = 0] = "A";
})(Enum || (Enum = {}));
var a = Enum.A;
var nameOfA = Enum[a]; // "A"
生成的代碼中,枚舉類型被編譯成一個對象,它包含雙向映射(name -> value)和(value -> name)。 引用枚舉成員總會生成一次屬性訪問而且永遠不會內聯。 在大多數狀況下這是很好的而且正確的解決方案。 然而有時候需求卻比較嚴格。 當訪問枚舉值時,爲了不生成多餘的代碼和間接引用,能夠使用常數枚舉。 常數枚舉是在enum關鍵字前使用const修飾符。
const enum Enum {
A = 1,
B = A 2
}
常數枚舉只能使用常數枚舉表達式而且不一樣於常規的枚舉的是它們在編譯階段會被刪除。 常數枚舉成員在使用的地方被內聯進來。 這是由於常數枚舉不可能有計算成員。
const enum Directions {
Up,
Down,
Left,
Right
}
let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right]
生成後的代碼爲:
var directions = [0 / Up /, 1 / Down /, 2 / Left /, 3 / Right /];
外部枚舉
外部枚舉用來描述已經存在的枚舉類型的形狀。
declare enum Enum {
A = 1,
B,
C = 2
}
外部枚舉和非外部枚舉之間有一個重要的區別,在正常的枚舉裏,沒有初始化方法的成員被當成常數成員。 對於很是數的外部枚舉而言,沒有初始化方法時被當作須要通過計算的。
類型推論(type inference)
這節介紹TypeScript裏的類型推論。即,類型是在哪裏如何被推斷的。
基礎
TypeScript裏,在有些沒有明確指出類型的地方,類型推論會幫助提供類型。以下面的例子
let x = 3;
變量x的類型被推斷爲數字。 這種推斷髮生在初始化變量和成員,設置默認參數值和決定函數返回值時。
大多數狀況下,類型推論是直截了當地。 後面的小節,咱們會瀏覽類型推論時的細微差異。
最佳通用類型
當須要從幾個表達式中推斷類型時候,會使用這些表達式的類型來推斷出一個最合適的通用類型。例如,
let x = [0, 1, null];
爲了推斷x的類型,咱們必須考慮全部元素的類型。 這裏有兩種選擇:number和null。 計算通用類型算法會考慮全部的候選類型,並給出一個兼容全部候選類型的類型。
因爲最終的通用類型取自候選類型,有些時候候選類型共享相同的通用類型,可是卻沒有一個類型能作爲全部候選類型的類型。例如:
let zoo = [new Rhino(), new Elephant(), new Snake()];
這裏,咱們想讓zoo被推斷爲Animal[]類型,可是這個數組裏沒有對象是Animal類型的,所以不能推斷出這個結果。 爲了更正,當候選類型不能使用的時候咱們須要明確的指出類型:
let zoo: Animal[] = [new Rhino(), new Elephant(), new Snake()];
若是沒有找到最佳通用類型的話,類型推斷的結果爲聯合數組類型,(Rhino | Elephant | Snake)[]。
上下文類型
TypeScript類型推論也可能按照相反的方向進行。 這被叫作「按上下文歸類」。按上下文歸類會發生在表達式的類型與所處的位置相關時。好比:
window.onmousedown = function(mouseEvent) {
console.log(mouseEvent.button); //<- Error
};
這個例子會獲得一個類型錯誤,TypeScript類型檢查器使用Window.onmousedown函數的類型來推斷右邊函數表達式的類型。 所以,就能推斷出mouseEvent參數的類型了。 若是函數表達式不是在上下文類型的位置,mouseEvent參數的類型須要指定爲any,這樣也不會報錯了。
若是上下文類型表達式包含了明確的類型信息,上下文的類型被忽略。 重寫上面的例子:
window.onmousedown = function(mouseEvent: any) {
console.log(mouseEvent.button); //<- Now, no error is given
};
這個函數表達式有明確的參數類型註解,上下文類型被忽略。 這樣的話就不報錯了,由於這裏不會使用到上下文類型。
上下文歸類會在不少狀況下使用到。 一般包含函數的參數,賦值表達式的右邊,類型斷言,對象成員和數組字面量和返回值語句。 上下文類型也會作爲最佳通用類型的候選類型。好比:
function createZoo(): Animal[] {
return [new Rhino(), new Elephant(), new Snake()];
}
這個例子裏,最佳通用類型有4個候選者:Animal,Rhino,Elephant和Snake。 固然,Animal會被作爲最佳通用類型。
類型兼容性
介紹
TypeScript裏的類型兼容性是基於結構子類型的。 結構類型是一種只使用其成員來描述類型的方式。 它正好與名義(nominal)類型造成對比。(譯者注:在基於名義類型的類型系統中,數據類型的兼容性或等價性是經過明確的聲明和/或類型的名稱來決定的。這與結構性類型系統不一樣,它是基於類型的組成結構,且不要求明確地聲明。) 看下面的例子:
interface Named {
name: string;
}
class Person {
name: string;
}
let p: Named;
// OK, because of structural typing
p = new Person();
在使用基於名義類型的語言,好比C#或Java中,這段代碼會報錯,由於Person類沒有明確說明其實現了Named接口。
TypeScript的結構性子類型是根據JavaScript代碼的典型寫法來設計的。 由於JavaScript裏普遍地使用匿名對象,例如函數表達式和對象字面量,因此使用結構類型系統來描述這些類型比使用名義類型系統更好。
關於可靠性的注意事項
TypeScript的類型系統容許某些在編譯階段沒法確認其安全性的操做。當一個類型系統具此屬性時,被當作是「不可靠」的。TypeScript容許這種不可靠行爲的發生是通過仔細考慮的。經過這篇文章,咱們會解釋何時會發生這種狀況和其有利的一面。
開始
TypeScript結構化類型系統的基本規則是,若是x要兼容y,那麼y至少具備與x相同的屬性。好比:
interface Named {
name: string;
}
let x: Named;
// y's inferred type is { name: string; location: string; }
let y = { name: 'Alice', location: 'Seattle' };
x = y;
這裏要檢查y是否能賦值給x,編譯器檢查x中的每一個屬性,看是否能在y中也找到對應屬性。 在這個例子中,y必須包含名字是name的string類型成員。y知足條件,所以賦值正確。
檢查函數參數時使用相同的規則:
function greet(n: Named) {
alert('Hello, ' + n.name);
}
greet(y); // OK
注意,y有個額外的location屬性,但這不會引起錯誤。 只有目標類型(這裏是Named)的成員會被一一檢查是否兼容。
這個比較過程是遞歸進行的,檢查每一個成員及子成員。
比較兩個函數
相對來說,在比較原始類型和對象類型的時候是比較容易理解的,問題是如何判斷兩個函數是兼容的。 下面咱們從兩個簡單的函數入手,它們僅是參數列表略有不一樣:
let x = (a: number) => 0;
let y = (b: number, s: string) => 0;
y = x; // OK
x = y; // Error
要查看x是否能賦值給y,首先看它們的參數列表。 x的每一個參數必須能在y裏找到對應類型的參數。 注意的是參數的名字相同與否無所謂,只看它們的類型。 這裏,x的每一個參數在y中都能找到對應的參數,因此容許賦值。
第二個賦值錯誤,由於y有個必需的第二個參數,可是x並無,因此不容許賦值。
你可能會疑惑爲何容許忽略參數,像例子y = x中那樣。 緣由是忽略額外的參數在JavaScript裏是很常見的。 例如,Array#forEach給回調函數傳3個參數:數組元素,索引和整個數組。 儘管如此,傳入一個只使用第一個參數的回調函數也是頗有用的:
let items = [1, 2, 3];
// Don't force these extra arguments
items.forEach((item, index, array) => console.log(item));
// Should be OK!
items.forEach((item) => console.log(item));
下面來看看如何處理返回值類型,建立兩個僅是返回值類型不一樣的函數:
let x = () => ({name: 'Alice'});
let y = () => ({name: 'Alice', location: 'Seattle'});
x = y; // OK
y = x; // Error because x() lacks a location property
類型系統強制源函數的返回值類型必須是目標函數返回值類型的子類型。
函數參數雙向協變
當比較函數參數類型時,只有當源函數參數可以賦值給目標函數或者反過來時才能賦值成功。 這是不穩定的,由於調用者可能傳入了一個具備更精確類型信息的函數,可是調用這個傳入的函數的時候卻使用了不是那麼精確的類型信息。 實際上,這極少會發生錯誤,而且可以實現不少JavaScript裏的常見模式。例如:
enum EventType { Mouse, Keyboard }
interface Event { timestamp: number; }
interface MouseEvent extends Event { x: number; y: number }
interface KeyEvent extends Event { keyCode: number }
function listenEvent(eventType: EventType, handler: (n: Event) => void) {
/ ... /
}
// Unsound, but useful and common
listenEvent(EventType.Mouse, (e: MouseEvent) => console.log(e.x + ',' + e.y));
// Undesirable alternatives in presence of soundness
listenEvent(EventType.Mouse, (e: Event) => console.log((<MouseEvent>e).x + ',' + (<MouseEvent>e).y));
listenEvent(EventType.Mouse, <(e: Event) => void>((e: MouseEvent) => console.log(e.x + ',' + e.y)));
// Still disallowed (clear error). Type safety enforced for wholly incompatible types
listenEvent(EventType.Mouse, (e: number) => console.log(e));
可選參數及剩餘參數
比較函數兼容性的時候,可選參數與必須參數是可互換的。 源類型上有額外的可選參數不是錯誤,目標類型的可選參數在源類型裏沒有對應的參數也不是錯誤。
當一個函數有剩餘參數時,它被當作無限個可選參數。
這對於類型系統來講是不穩定的,但從運行時的角度來看,可選參數通常來講是不強制的,由於對於大多數函數來講至關於傳遞了一些undefinded。
有一個好的例子,常見的函數接收一個回調函數並用對於程序員來講是可預知的參數但對類型系統來講是不肯定的參數來調用:
function invokeLater(args: any[], callback: (...args: any[]) => void) {
/ ... Invoke callback with 'args' ... /
}
// Unsound - invokeLater "might" provide any number of arguments
invokeLater([1, 2], (x, y) => console.log(x + ', ' + y));
// Confusing (x and y are actually required) and undiscoverable
invokeLater([1, 2], (x?, y?) => console.log(x + ', ' + y));
函數重載
對於有重載的函數,源函數的每一個重載都要在目標函數上找到對應的函數簽名。 這確保了目標函數能夠在全部源函數可調用的地方調用。
枚舉
枚舉類型與數字類型兼容,而且數字類型與枚舉類型兼容。不一樣枚舉類型之間是不兼容的。好比,
enum Status { Ready, Waiting };
enum Color { Red, Blue, Green };
let status = Status.Ready;
status = Color.Green; //error
類
類與對象字面量和接口差很少,但有一點不一樣:類有靜態部分和實例部分的類型。 比較兩個類類型的對象時,只有實例的成員會被比較。 靜態成員和構造函數不在比較的範圍內。
class Animal {
feet: number;
constructor(name: string, numFeet: number) { }
}
class Size {
feet: number;
constructor(numFeet: number) { }
}
let a: Animal;
let s: Size;
a = s; //OK
s = a; //OK
類的私有成員
私有成員會影響兼容性判斷。 當類的實例用來檢查兼容時,若是目標類型包含一個私有成員,那麼源類型必須包含來自同一個類的這個私有成員。 這容許子類賦值給父類,可是不能賦值給其它有一樣類型的類。
泛型
由於TypeScript是結構性的類型系統,類型參數隻影響使用其作爲類型一部分的結果類型。好比,
interface Empty<T> {
}
let x: Empty<number>;
let y: Empty<string>;
x = y; // okay, y matches structure of x
上面代碼裏,x和y是兼容的,由於它們的結構使用類型參數時並無什麼不一樣。 把這個例子改變一下,增長一個成員,就能看出是如何工做的了:
interface NotEmpty<T> {
data: T;
}
let x: NotEmpty<number>;
let y: NotEmpty<string>;
x = y; // error, x and y are not compatible
在這裏,泛型類型在使用時就比如不是一個泛型類型。
對於沒指定泛型類型的泛型參數時,會把全部泛型參數當成any比較。 而後用結果類型進行比較,就像上面第一個例子。
好比,
let identity = function<T>(x: T): T {
// ...
}
let reverse = function<U>(y: U): U {
// ...
}
identity = reverse; // Okay because (x: any)=>any matches (y: any)=>any
高級主題
子類型與賦值
目前爲止,咱們使用了兼容性,它在語言規範裏沒有定義。 在TypeScript裏,有兩種類型的兼容性:子類型與賦值。 它們的不一樣點在於,賦值擴展了子類型兼容,容許給any賦值或從any取值和容許數字賦值給枚舉類型或枚舉類型賦值給數字。
語言裏的不一樣地方分別使用了它們之中的機制。 實際上,類型兼容性是由賦值兼容性來控制的,即便在implements和extends語句也不例外。
高級類型
交叉類型(Intersection Types)
交叉類型是將多個類型合併爲一個類型。 這讓咱們能夠把現有的多種類型疊加到一塊兒成爲一種類型,它包含了所需的全部類型的特性。 例如,Person & Serializable & Loggable同時是Person和Serializable和Loggable。 就是說這個類型的對象同時擁有了這三種類型的成員。
咱們大可能是在混入(mixins)或其它不適合典型面向對象模型的地方看到交叉類型的使用。 (在JavaScript裏發生這種狀況的場合不少!) 下面是如何建立混入的一個簡單例子:
function extend<T, U>(first: T, second: U): T & U {
let result = <T & U>{};
for (let id in first) {
(<any>result)[id] = (<any>first)[id];
}
for (let id in second) {
if (!result.hasOwnProperty(id)) {
(<any>result)[id] = (<any>second)[id];
}
}
return result;
}
class Person {
constructor(public name: string) { }
}
interface Loggable {
log(): void;
}
class ConsoleLogger implements Loggable {
log() {
// ...
}
}
var jim = extend(new Person("Jim"), new ConsoleLogger());
var n = jim.name;
jim.log();
聯合類型(Union Types)
聯合類型與交叉類型頗有關聯,可是使用上卻徹底不一樣。 偶爾你會遇到這種狀況,一個代碼庫但願傳入number或string類型的參數。 例以下面的函數:
/**
Expected string or number, got '${padding}'.
);padLeft("Hello world", 4); // returns " Hello world"
padLeft存在一個問題,padding參數的類型指定成了any。 這就是說咱們能夠傳入一個既不是number也不是string類型的參數,可是TypeScript卻不報錯。
let indentedString = padLeft("Hello world", true); // 編譯階段經過,運行時報錯
在傳統的面嚮對象語言裏,咱們可能會將這兩種類型抽象成有層級的類型。 這麼作顯然是很是清晰的,但同時也存在了過分設計。 padLeft原始版本的好處之一是容許咱們傳入原始類型。 這樣作的話使用起來既簡單又方便。 若是咱們就是想使用已經存在的函數的話,這種新的方式就不適用了。
代替any, 咱們能夠使用聯合類型作爲padding的參數:
/**
let indentedString = padLeft("Hello world", true); // errors during compilation
聯合類型表示一個值能夠是幾種類型之一。 咱們用豎線(|)分隔每一個類型,因此number | string | boolean表示一個值能夠是number,string,或boolean。
若是一個值是聯合類型,咱們只能訪問此聯合類型的全部類型裏共有的成員。
interface Bird {
fly();
layEggs();
}
interface Fish {
swim();
layEggs();
}
function getSmallPet(): Fish | Bird {
// ...
}
let pet = getSmallPet();
pet.layEggs(); // okay
pet.swim(); // errors
這裏的聯合類型可能有點複雜,可是你很容易就習慣了。 若是一個值的類型是A | B,咱們可以肯定的是它包含了A和B中共有的成員。 這個例子裏,Bird具備一個fly成員。 咱們不能肯定一個Bird | Fish類型的變量是否有fly方法。 若是變量在運行時是Fish類型,那麼調用pet.fly()就出錯了。
類型保護與區分類型(Type Guards and Differentiating Types)
聯合類型適合於那些值能夠爲不一樣類型的狀況。 但當咱們想確切地瞭解是否爲Fish時怎麼辦? JavaScript裏經常使用來區分2個可能值的方法是檢查成員是否存在。 如以前說起的,咱們只能訪問聯合類型中共同擁有的成員。
let pet = getSmallPet();
// 每個成員訪問都會報錯
if (pet.swim) {
pet.swim();
}
else if (pet.fly) {
pet.fly();
}
爲了讓這段代碼工做,咱們要使用類型斷言:
let pet = getSmallPet();
if ((<Fish>pet).swim) {
(<Fish>pet).swim();
}
else {
(<Bird>pet).fly();
}
用戶自定義的類型保護
這裏能夠注意到咱們不得很少次使用類型斷言。 倘若咱們一旦檢查過類型,就能在以後的每一個分支裏清楚地知道pet的類型的話就行了。
TypeScript裏的類型保護機制讓它成爲了現實。 類型保護就是一些表達式,它們會在運行時檢查以確保在某個做用域裏的類型。 要定義一個類型保護,咱們只要簡單地定義一個函數,它的返回值是一個類型謂詞:
function isFish(pet: Fish | Bird): pet is Fish {
return (<Fish>pet).swim !== undefined;
}
在這個例子裏,pet is Fish就是類型謂詞。 謂詞爲parameterName is Type這種形式,parameterName必須是來自於當前函數簽名裏的一個參數名。
每當使用一些變量調用isFish時,TypeScript會將變量縮減爲那個具體的類型,只要這個類型與變量的原始類型是兼容的。
// 'swim' 和 'fly' 調用都沒有問題了
if (isFish(pet)) {
pet.swim();
}
else {
pet.fly();
}
注意TypeScript不只知道在if分支裏pet是Fish類型; 它還清楚在else分支裏,必定不是Fish類型,必定是Bird類型。
typeof類型保護
如今咱們回過頭來看看怎麼使用聯合類型書寫padLeft代碼。 咱們能夠像下面這樣利用類型斷言來寫:
function isNumber(x: any): x is number {
return typeof x === "number";
}
function isString(x: any): x is string {
return typeof x === "string";
}
function padLeft(value: string, padding: string | number) {
if (isNumber(padding)) {
return Array(padding + 1).join(" ") + value;
}
if (isString(padding)) {
return padding + value;
}
throw new Error(Expected string or number, got '${padding}'.
);
}
然而,必需要定義一個函數來判斷類型是不是原始類型,這太痛苦了。 幸運的是,如今咱們沒必要將typeof x === "number"抽象成一個函數,由於TypeScript能夠將它識別爲一個類型保護。 也就是說咱們能夠直接在代碼裏檢查類型了。
function padLeft(value: string, padding: string | number) {
if (typeof padding === "number") {
return Array(padding + 1).join(" ") + value;
}
if (typeof padding === "string") {
return padding + value;
}
throw new Error(Expected string or number, got '${padding}'.
);
}
這些typeof類型保護只有兩種形式能被識別:typeof v === "typename"和typeof v !== "typename","typename"必須是"number","string","boolean"或"symbol"。 可是TypeScript並不會阻止你與其它字符串比較,語言不會把那些表達式識別爲類型保護。
instanceof類型保護
若是你已經閱讀了typeof類型保護而且對JavaScript裏的instanceof操做符熟悉的話,你可能已經猜到了這節要講的內容。
instanceof類型保護是經過構造函數來細化類型的一種方式。 好比,咱們借鑑一下以前字符串填充的例子:
interface Padder {
getPaddingString(): string
}
class SpaceRepeatingPadder implements Padder {
constructor(private numSpaces: number) { }
getPaddingString() {
return Array(this.numSpaces + 1).join(" ");
}
}
class StringPadder implements Padder {
constructor(private value: string) { }
getPaddingString() {
return this.value;
}
}
function getRandomPadder() {
return Math.random() < 0.5 ?
new SpaceRepeatingPadder(4) :
new StringPadder(" ");
}
// 類型爲SpaceRepeatingPadder | StringPadder
let padder: Padder = getRandomPadder();
if (padder instanceof SpaceRepeatingPadder) {
padder; // 類型細化爲'SpaceRepeatingPadder'
}
if (padder instanceof StringPadder) {
padder; // 類型細化爲'StringPadder'
}
instanceof的右側要求是一個構造函數,TypeScript將細化爲:
此構造函數的prototype屬性的類型,若是它的類型不爲any的話
構造簽名所返回的類型的聯合
以此順序。
能夠爲null的類型
TypeScript具備兩種特殊的類型,null和undefined,它們分別具備值null和undefined. 咱們在基礎類型一節裏已經作過簡要說明。 默認狀況下,類型檢查器認爲null與undefined能夠賦值給任何類型。 null與undefined是全部其它類型的一個有效值。 這也意味着,你阻止不了將它們賦值給其它類型,就算是你想要阻止這種狀況也不行。 null的發明者,Tony Hoare,稱它爲價值億萬美金的錯誤。
--strictNullChecks標記能夠解決此錯誤:當你聲明一個變量時,它不會自動地包含null或undefined。 你能夠使用聯合類型明確的包含它們:
let s = "foo";
s = null; // 錯誤, 'null'不能賦值給'string'
let sn: string | null = "bar";
sn = null; // 能夠
sn = undefined; // error, 'undefined'不能賦值給'string | null'
注意,按照JavaScript的語義,TypeScript會把null和undefined區別對待。 string | null,string | undefined和string | undefined | null是不一樣的類型。
可選參數和可選屬性
使用了--strictNullChecks,可選參數會被自動地加上| undefined:
function f(x: number, y?: number) {
return x + (y || 0);
}
f(1, 2);
f(1);
f(1, undefined);
f(1, null); // error, 'null' is not assignable to 'number | undefined'
可選屬性也會有一樣的處理:
class C {
a: number;
b?: number;
}
let c = new C();
c.a = 12;
c.a = undefined; // error, 'undefined' is not assignable to 'number'
c.b = 13;
c.b = undefined; // ok
c.b = null; // error, 'null' is not assignable to 'number | undefined'
類型保護和類型斷言
因爲能夠爲null的類型是經過聯合類型實現,那麼你須要使用類型保護來去除null。 幸運地是這與在JavaScript裏寫的代碼一致:
function f(sn: string | null): string {
if (sn == null) {
return "default";
}
else {
return sn;
}
}
這裏很明顯地去除了null,你也能夠使用短路運算符:
function f(sn: string | null): string {
return sn || "default";
}
若是編譯器不可以去除null或undefined,你能夠使用類型斷言手動去除。 語法是添加!後綴:identifier!從identifier的類型裏去除了null和undefined:
function broken(name: string | null): string {
function postfix(epithet: string) {
return name.charAt(0) + '. the ' + epithet; // error, 'name' is possibly null
}
name = name || "Bob";
return postfix("great");
}
function fixed(name: string | null): string {
function postfix(epithet: string) {
return name!.charAt(0) + '. the ' + epithet; // ok
}
name = name || "Bob";
return postfix("great");
}
本例使用了嵌套函數,由於編譯器沒法去除嵌套函數的null(除非是當即調用的函數表達式)。 由於它沒法跟蹤全部對嵌套函數的調用,尤爲是你將內層函數作爲外層函數的返回值。 若是沒法知道函數在哪裏被調用,就沒法知道調用時name的類型。
類型別名
類型別名會給一個類型起個新名字。 類型別名有時和接口很像,可是能夠做用於原始值,聯合類型,元組以及其它任何你須要手寫的類型。
type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
if (typeof n === 'string') {
return n;
}
else {
return n();
}
}
起別名不會新建一個類型 - 它建立了一個新名字來引用那個類型。 給原始類型起別名一般沒什麼用,儘管能夠作爲文檔的一種形式使用。
同接口同樣,類型別名也能夠是泛型 - 咱們能夠添加類型參數而且在別名聲明的右側傳入:
type Container<T> = { value: T };
咱們也能夠使用類型別名來在屬性裏引用本身:
type Tree<T> = {
value: T;
left: Tree<T>;
right: Tree<T>;
}
與交叉類型一塊兒使用,咱們能夠建立出一些十分稀奇古怪的類型。
type LinkedList<T> = T & { next: LinkedList<T> };
interface Person {
name: string;
}
var people: LinkedList<Person>;
var s = people.name;
var s = people.next.name;
var s = people.next.next.name;
var s = people.next.next.next.name;
然而,類型別名不能出如今聲明右側的任何地方。
type Yikes = Array<Yikes>; // error
接口 vs. 類型別名
像咱們提到的,類型別名能夠像接口同樣;然而,仍有一些細微差異。
其一,接口建立了一個新的名字,能夠在其它任何地方使用。 類型別名並不建立新名字—好比,錯誤信息就不會使用別名。 在下面的示例代碼裏,在編譯器中將鼠標懸停在interfaced上,顯示它返回的是Interface,但懸停在aliased上時,顯示的倒是對象字面量類型。
type Alias = { num: number }
interface Interface {
num: number;
}
declare function aliased(arg: Alias): Alias;
declare function interfaced(arg: Interface): Interface;
另外一個重要區別是類型別名不能被extends和implements(本身也不能extends和implements其它類型)。 由於軟件中的對象應該對於擴展是開放的,可是對於修改是封閉的,你應該儘可能去使用接口代替類型別名。
另外一方面,若是你沒法經過接口來描述一個類型而且須要使用聯合類型或元組類型,這時一般會使用類型別名。
字符串字面量類型
字符串字面量類型容許你指定字符串必須的固定值。 在實際應用中,字符串字面量類型能夠與聯合類型,類型保護和類型別名很好的配合。 經過結合使用這些特性,你能夠實現相似枚舉類型的字符串。
type Easing = "ease-in" | "ease-out" | "ease-in-out";
class UIElement {
animate(dx: number, dy: number, easing: Easing) {
if (easing === "ease-in") {
// ...
}
else if (easing === "ease-out") {
}
else if (easing === "ease-in-out") {
}
else {
// error! should not pass null or undefined.
}
}
}
let button = new UIElement();
button.animate(0, 0, "ease-in");
button.animate(0, 0, "uneasy"); // error: "uneasy" is not allowed here
你只能從三種容許的字符中選擇其一來作爲參數傳遞,傳入其它值則會產生錯誤。
Argument of type '"uneasy"' is not assignable to parameter of type '"ease-in" | "ease-out" | "ease-in-out"'
字符串字面量類型還能夠用於區分函數重載:
function createElement(tagName: "img"): HTMLImageElement;
function createElement(tagName: "input"): HTMLInputElement;
// ... more overloads ...
function createElement(tagName: string): Element {
// ... code goes here ...
}
數字字面量類型
TypeScript還具備數字字面量類型。
function rollDie(): 1 | 2 | 3 | 4 | 5 | 6 {
// ...
}
咱們不多直接這樣使用,但它們能夠用在縮小範圍調試bug的時候:
function foo(x: number) {
if (x !== 1 || x !== 2) {
// ~~~
// Operator '!==' cannot be applied to types '1' and '2'.
}
}
換句話說,當x與2進行比較的時候,它的值必須爲1,這就意味着上面的比較檢查是非法的。
枚舉成員類型
如咱們在枚舉一節裏提到的,當每一個枚舉成員都是用字面量初始化的時候枚舉成員是具備類型的。
在咱們談及「單例類型」的時候,多數是指枚舉成員類型和數字/字符串字面量類型,儘管大多數用戶會互換使用「單例類型」和「字面量類型」。
可辨識聯合(Discriminated Unions)
你能夠合併單例類型,聯合類型,類型保護和類型別名來建立一個叫作可辨識聯合的高級模式,它也稱作標籤聯合或代數數據類型。 可辨識聯合在函數式編程頗有用處。 一些語言會自動地爲你辨識聯合;而TypeScript則基於已有的JavaScript模式。 它具備3個要素:
具備普通的單例類型屬性—可辨識的特徵。
一個類型別名包含了那些類型的聯合—聯合。
此屬性上的類型保護。
interface Square {
kind: "square";
size: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
interface Circle {
kind: "circle";
radius: number;
}
首先咱們聲明瞭將要聯合的接口。 每一個接口都有kind屬性但有不一樣的字符串字面量類型。 kind屬性稱作可辨識的特徵或標籤。 其它的屬性則特定於各個接口。 注意,目前各個接口間是沒有聯繫的。 下面咱們把它們聯合到一塊兒:
type Shape = Square | Rectangle | Circle;
如今咱們使用可辨識聯合:
function area(s: Shape) {
switch (s.kind) {
case "square": return s.size s.size;
case "rectangle": return s.height s.width;
case "circle": return Math.PI * s.radius 2;
}
}
完整性檢查
當沒有涵蓋全部可辨識聯合的變化時,咱們想讓編譯器能夠通知咱們。 好比,若是咱們添加了Triangle到Shape,咱們同時還須要更新area:
type Shape = Square | Rectangle | Circle | Triangle;
function area(s: Shape) {
switch (s.kind) {
case "square": return s.size s.size;
case "rectangle": return s.height s.width;
case "circle": return Math.PI * s.radius * 2;
}
// should error here - we didn't handle case "triangle"
}
有兩種方式能夠實現。 首先是啓用--strictNullChecks而且指定一個返回值類型:
function area(s: Shape): number { // error: returns number | undefined
switch (s.kind) {
case "square": return s.size s.size;
case "rectangle": return s.height s.width;
case "circle": return Math.PI s.radius 2;
}
}
由於switch沒有包涵全部狀況,因此TypeScript認爲這個函數有時候會返回undefined。 若是你明確地指定了返回值類型爲number,那麼你會看到一個錯誤,由於實際上返回值的類型爲number | undefined。 然而,這種方法存在些微妙之處且--strictNullChecks對舊代碼支持很差。
第二種方法使用never類型,編譯器用它來進行完整性檢查:
function assertNever(x: never): never {
throw new Error("Unexpected object: " + x);
}
function area(s: Shape) {
switch (s.kind) {
case "square": return s.size s.size;
case "rectangle": return s.height s.width;
case "circle": return Math.PI * s.radius * 2;
default: return assertNever(s); // error here if there are missing cases
}
}
這裏,assertNever檢查s是否爲never類型—即爲除去全部可能狀況後剩下的類型。 若是你忘記了某個case,那麼s將具備一個真實的類型而且你會獲得一個錯誤。 這種方式須要你定義一個額外的函數,可是在你忘記某個case的時候也更加明顯。
多態的this類型
多態的this類型表示的是某個包含類或接口的子類型。 這被稱作F-bounded多態性。 它能很容易的表現連貫接口間的繼承,好比。 在計算器的例子裏,在每一個操做以後都返回this類型:
class BasicCalculator {
public constructor(protected value: number = 0) { }
public currentValue(): number {
return this.value;
}
public add(operand: number): this {
this.value += operand;
return this;
}
public multiply(operand: number): this {
this.value = operand;
return this;
}
// ... other operations go here ...
}
let v = new BasicCalculator(2)
.multiply(5)
.add(1)
.currentValue();
因爲這個類使用了this類型,你能夠繼承它,新的類能夠直接使用以前的方法,不須要作任何的改變。
class ScientificCalculator extends BasicCalculator {
public constructor(value = 0) {
super(value);
}
public sin() {
this.value = Math.sin(this.value);
return this;
}
// ... other operations go here ...
}
let v = new ScientificCalculator(2)
.multiply(5)
.sin()
.add(1)
.currentValue();
若是沒有this類型,ScientificCalculator就不可以在繼承BasicCalculator的同時還保持接口的連貫性。 multiply將會返回BasicCalculator,它並無sin方法。 然而,使用this類型,multiply會返回this,在這裏就是ScientificCalculator。
索引類型(Index types)
使用索引類型,編譯器就可以檢查使用了動態屬性名的代碼。 例如,一個常見的JavaScript模式是從對象中選取屬性的子集。
function pluck(o, names) {
return names.map(n => o[n]);
}
下面是如何在TypeScript裏使用此函數,經過索引類型查詢和索引訪問操做符:
function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] {
return names.map(n => o[n]);
}
interface Person {
name: string;
age: number;
}
let person: Person = {
name: 'Jarid',
age: 35
};
let strings: string[] = pluck(person, ['name']); // ok, string[]
編譯器會檢查name是否真的是Person的一個屬性。 本例還引入了幾個新的類型操做符。 首先是keyof T,索引類型查詢操做符。 對於任何類型T,keyof T的結果爲T上已知的公共屬性名的聯合。 例如:
let personProps: keyof Person; // 'name' | 'age'
keyof Person是徹底能夠與'name' | 'age'互相替換的。 不一樣的是若是你添加了其它的屬性到Person,例如address: string,那麼keyof Person會自動變爲'name' | 'age' | 'address'。 你能夠在像pluck函數這類上下文裏使用keyof,由於在使用以前你並不清楚可能出現的屬性名。 但編譯器會檢查你是否傳入了正確的屬性名給pluck:
pluck(person, ['age', 'unknown']); // error, 'unknown' is not in 'name' | 'age'
第二個操做符是T[K],索引訪問操做符。 在這裏,類型語法反映了表達式語法。 這意味着person['name']具備類型Person['name'] — 在咱們的例子裏則爲string類型。 然而,就像索引類型查詢同樣,你能夠在普通的上下文裏使用T[K],這正是它的強大所在。 你只要確保類型變量K extends keyof T就能夠了。 例以下面getProperty函數的例子:
function getProperty<T, K extends keyof T>(o: T, name: K): T[K] {
return o[name]; // o[name] is of type T[K]
}
getProperty裏的o: T和name: K,意味着o[name]: T[K]。 當你返回T[K]的結果,編譯器會實例化鍵的真實類型,所以getProperty的返回值類型會隨着你須要的屬性改變。
let name: string = getProperty(person, 'name');
let age: number = getProperty(person, 'age');
let unknown = getProperty(person, 'unknown'); // error, 'unknown' is not in 'name' | 'age'
索引類型和字符串索引簽名
keyof和T[K]與字符串索引簽名進行交互。 若是你有一個帶有字符串索引簽名的類型,那麼keyof T會是string。 而且T[string]爲索引簽名的類型:
interface Map<T> {
}
let keys: keyof Map<number>; // string
let value: Map<number>['foo']; // number
映射類型
一個常見的任務是將一個已知的類型每一個屬性都變爲可選的:
interface PersonPartial {
name?: string;
age?: number;
}
或者咱們想要一個只讀版本:
interface PersonReadonly {
readonly name: string;
readonly age: number;
}
這在JavaScript裏常常出現,TypeScript提供了從舊類型中建立新類型的一種方式 — 映射類型。 在映射類型裏,新類型以相同的形式去轉換舊類型裏每一個屬性。 例如,你能夠令每一個屬性成爲readonly類型或可選的。 下面是一些例子:
type Readonly<T> = {
readonly P in keyof T: T[P];
}
type Partial<T> = {
P in keyof T?: T[P];
}
像下面這樣使用:
type PersonPartial = Partial<Person>;
type ReadonlyPerson = Readonly<Person>;
下面來看看最簡單的映射類型和它的組成部分:
type Keys = 'option1' | 'option2';
type Flags = { [K in Keys]: boolean };
它的語法與索引簽名的語法類型,內部使用了for .. in。 具備三個部分:
類型變量K,它會依次綁定到每一個屬性。
字符串字面量聯合的Keys,它包含了要迭代的屬性名的集合。
屬性的結果類型。
在個簡單的例子裏,Keys是硬編碼的的屬性名列表而且屬性類型永遠是boolean,所以這個映射類型等同於:
type Flags = {
option1: boolean;
option2: boolean;
}
在真正的應用裏,可能不一樣於上面的Readonly或Partial。 它們會基於一些已存在的類型,且按照必定的方式轉換字段。 這就是keyof和索引訪問類型要作的事情:
type NullablePerson = { [P in keyof Person]: Person[P] | null }
type PartialPerson = { [P in keyof Person]?: Person[P] }
但它更有用的地方是能夠有一些通用版本。
type Nullable<T> = { P in keyof T: T[P] | null }
type Partial<T> = { P in keyof T?: T[P] }
在這些例子裏,屬性列表是keyof T且結果類型是T[P]的變體。 這是使用通用映射類型的一個好模版。 由於這類轉換是同態的,映射只做用於T的屬性而沒有其它的。 編譯器知道在添加任何新屬性以前能夠拷貝全部存在的屬性修飾符。 例如,假設Person.name是隻讀的,那麼Partial<Person>.name也將是隻讀的且爲可選的。
下面是另外一個例子,T[P]被包裝在Proxy<T>類裏:
type Proxy<T> = {
get(): T;
set(value: T): void;
}
type Proxify<T> = {
}
function proxify<T>(o: T): Proxify<T> {
// ... wrap proxies ...
}
let proxyProps = proxify(props);
注意Readonly<T>和Partial<T>用處不小,所以它們與Pick和Record一同被包含進了TypeScript的標準庫裏:
type Pick<T, K extends keyof T> = {
}
type Record<K extends string, T> = {
}
Readonly,Partial和Pick是同態的,但Record不是。 由於Record並不須要輸入類型來拷貝屬性,因此它不屬於同態:
type ThreeStringProps = Record<'prop1' | 'prop2' | 'prop3', string>
非同態類型本質上會建立新的屬性,所以它們不會從它處拷貝屬性修飾符。
由映射類型進行推斷
如今你瞭解瞭如何包裝一個類型的屬性,那麼接下來就是如何拆包。 其實這也很是容易:
function unproxify<T>(t: Proxify<T>): T {
let result = {} as T;
for (const k in t) {
result[k] = t[k].get();
}
return result;
}
let originalProps = unproxify(proxyProps);
Symbols
介紹
自ECMAScript 2015起,symbol成爲了一種新的原生類型,就像number和string同樣。
symbol類型的值是經過Symbol構造函數建立的。
let sym1 = Symbol();
let sym2 = Symbol("key"); // 可選的字符串key
Symbols是不可改變且惟一的。
let sym2 = Symbol("key");
let sym3 = Symbol("key");
sym2 === sym3; // false, symbols是惟一的
像字符串同樣,symbols也能夠被用作對象屬性的鍵。
let sym = Symbol();
let obj = {
};
console.log(objsym); // "value"
Symbols也能夠與計算出的屬性名聲明相結合來聲明對象的屬性和類成員。
const getClassNameSymbol = Symbol();
class C {
[getClassNameSymbol](){
return "C";
}
}
let c = new C();
let className = c[getClassNameSymbol](); // "C"
衆所周知的Symbols
除了用戶定義的symbols,還有一些已經衆所周知的內置symbols。 內置symbols用來表示語言內部的行爲。
如下爲這些symbols的列表:
Symbol.hasInstance
方法,會被instanceof運算符調用。構造器對象用來識別一個對象是不是其實例。
Symbol.isConcatSpreadable
布爾值,表示當在一個對象上調用Array.prototype.concat時,這個對象的數組元素是否可展開。
Symbol.iterator
方法,被for-of語句調用。返回對象的默認迭代器。
Symbol.match
方法,被String.prototype.match調用。正則表達式用來匹配字符串。
Symbol.replace
方法,被String.prototype.replace調用。正則表達式用來替換字符串中匹配的子串。
Symbol.search
方法,被String.prototype.search調用。正則表達式返回被匹配部分在字符串中的索引。
Symbol.species
函數值,爲一個構造函數。用來建立派生對象。
Symbol.split
方法,被String.prototype.split調用。正則表達式來用分割字符串。
Symbol.toPrimitive
方法,被ToPrimitive抽象操做調用。把對象轉換爲相應的原始值。
Symbol.toStringTag
方法,被內置方法Object.prototype.toString調用。返回建立對象時默認的字符串描述。
Symbol.unscopables
對象,它本身擁有的屬性會被with做用域排除在外。
Iterators 和 Generators
可迭代性
當一個對象實現了Symbol.iterator屬性時,咱們認爲它是可迭代的。 一些內置的類型如Array,Map,Set,String,Int32Array,Uint32Array等都已經實現了各自的Symbol.iterator。 對象上的Symbol.iterator函數負責返回供迭代的值。
for..of 語句
for..of會遍歷可迭代的對象,調用對象上的Symbol.iterator方法。 下面是在數組上使用for..of的簡單例子:
let someArray = [1, "string", false];
for (let entry of someArray) {
console.log(entry); // 1, "string", false
}
for..of vs. for..in 語句
for..of和for..in都可迭代一個列表;可是用於迭代的值卻不一樣,for..in迭代的是對象的 鍵 的列表,而for..of則迭代對象的鍵對應的值。
下面的例子展現了二者之間的區別:
let list = [4, 5, 6];
for (let i in list) {
console.log(i); // "0", "1", "2",
}
for (let i of list) {
console.log(i); // "4", "5", "6"
}
另外一個區別是for..in能夠操做任何對象;它提供了查看對象屬性的一種方法。 可是for..of關注於迭代對象的值。內置對象Map和Set已經實現了Symbol.iterator方法,讓咱們能夠訪問它們保存的值。
let pets = new Set(["Cat", "Dog", "Hamster"]);
pets["species"] = "mammals";
for (let pet in pets) {
console.log(pet); // "species"
}
for (let pet of pets) {
console.log(pet); // "Cat", "Dog", "Hamster"
}
代碼生成
目標爲 ES5 和 ES3
當生成目標爲ES5或ES3,迭代器只容許在Array類型上使用。 在非數組值上使用for..of語句會獲得一個錯誤,就算這些非數組值已經實現了Symbol.iterator屬性。
編譯器會生成一個簡單的for循環作爲for..of循環,好比:
let numbers = [1, 2, 3];
for (let num of numbers) {
console.log(num);
}
生成的代碼爲:
var numbers = [1, 2, 3];
for (var _i = 0; _i < numbers.length; _i++) {
var num = numbers[_i];
console.log(num);
}
模塊
ypeScript與ECMAScript 2015同樣,任何包含頂級import或者export的文件都被當成一個模塊。
導出
導出聲明
任何聲明(好比變量,函數,類,類型別名或接口)都可以經過添加export關鍵字來導出。
Validation.ts
export interface StringValidator {
isAcceptable(s: string): boolean;
}
ZipCodeValidator.ts
export const numberRegexp = /^[0-9]+$/;
export class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
導出語句
導出語句很便利,由於咱們可能須要對導出的部分重命名,因此上面的例子能夠這樣改寫:
class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
export { ZipCodeValidator };
export { ZipCodeValidator as mainValidator };
從新導出
咱們常常會去擴展其它模塊,而且只導出那個模塊的部份內容。 從新導出功能並不會在當前模塊導入那個模塊或定義一個新的局部變量。
ParseIntBasedZipCodeValidator.ts
export class ParseIntBasedZipCodeValidator {
isAcceptable(s: string) {
return s.length === 5 && parseInt(s).toString() === s;
}
}
// 導出原先的驗證器但作了重命名
export {ZipCodeValidator as RegExpBasedZipCodeValidator} from "./ZipCodeValidator";
或者一個模塊能夠包裹多個模塊,並把他們導出的內容聯合在一塊兒經過語法:export from "module"。
AllValidators.ts
export from "./StringValidator"; // exports interface StringValidator
export from "./LettersOnlyValidator"; // exports class LettersOnlyValidator
export from "./ZipCodeValidator"; // exports class ZipCodeValidator
導入
模塊的導入操做與導出同樣簡單。 能夠使用如下import形式之一來導入其它模塊中的導出內容。
導入一個模塊中的某個導出內容
import { ZipCodeValidator } from "./ZipCodeValidator";
let myValidator = new ZipCodeValidator();
能夠對導入內容重命名
import { ZipCodeValidator as ZCV } from "./ZipCodeValidator";
let myValidator = new ZCV();
將整個模塊導入到一個變量,並經過它來訪問模塊的導出部分
import * as validator from "./ZipCodeValidator";
let myValidator = new validator.ZipCodeValidator();
具備反作用的導入模塊
儘管不推薦這麼作,一些模塊會設置一些全局狀態供其它模塊使用。 這些模塊可能沒有任何的導出或用戶根本就不關注它的導出。 使用下面的方法來導入這類模塊:
import "./my-module.js";
默認導出
每一個模塊均可以有一個default導出。 默認導出使用default關鍵字標記;而且一個模塊只可以有一個default導出。 須要使用一種特殊的導入形式來導入default導出。
default導出十分便利。 好比,像JQuery這樣的類庫可能有一個默認導出jQuery或$,而且咱們基本上也會使用一樣的名字jQuery或$導出JQuery。
JQuery.d.ts
declare let $: JQuery;
export default $;
App.ts
import $ from "JQuery";
$("button.continue").html( "Next Step..." );
類和函數聲明能夠直接被標記爲默認導出。 標記爲默認導出的類和函數的名字是能夠省略的。
ZipCodeValidator.ts
export default class ZipCodeValidator {
static numberRegexp = /^[0-9]+$/;
isAcceptable(s: string) {
return s.length === 5 && ZipCodeValidator.numberRegexp.test(s);
}
}
Test.ts
import validator from "./ZipCodeValidator";
let myValidator = new validator();
或者
StaticZipCodeValidator.ts
const numberRegexp = /^[0-9]+$/;
export default function (s: string) {
return s.length === 5 && numberRegexp.test(s);
}
Test.ts
import validate from "./StaticZipCodeValidator";
let strings = ["Hello", "98052", "101"];
// Use function validate
strings.forEach(s => {
console.log("${s}" ${validate(s) ? " matches" : " does not match"}
);
});
default導出也能夠是一個值
OneTwoThree.ts
export default "123";
Log.ts
import num from "./OneTwoThree";
console.log(num); // "123"
export = 和 import = require()
CommonJS和AMD都有一個exports對象的概念,它包含了一個模塊的全部導出內容。
它們也支持把exports替換爲一個自定義對象。 默認導出就比如這樣一個功能;然而,它們卻並不相互兼容。 TypeScript模塊支持export =語法以支持傳統的CommonJS和AMD的工做流模型。
export =語法定義一個模塊的導出對象。 它能夠是類,接口,命名空間,函數或枚舉。
若要導入一個使用了export =的模塊時,必須使用TypeScript提供的特定語法import module = require("module")。
ZipCodeValidator.ts
let numberRegexp = /^[0-9]+$/;
class ZipCodeValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
export = ZipCodeValidator;
Test.ts
import zip = require("./ZipCodeValidator");
// Some samples to try
let strings = ["Hello", "98052", "101"];
// Validators to use
let validator = new zip();
// Show whether each string passed each validator
strings.forEach(s => {
console.log("${ s }" - ${ validator.isAcceptable(s) ? "matches" : "does not match" }
);
});
生成模塊代碼
根據編譯時指定的模塊目標參數,編譯器會生成相應的供Node.js (CommonJS),Require.js (AMD),isomorphic (UMD), SystemJS或ECMAScript 2015 native modules (ES6)模塊加載系統使用的代碼。 想要了解生成代碼中define,require 和 register的意義,請參考相應模塊加載器的文檔。
下面的例子說明了導入導出語句裏使用的名字是怎麼轉換爲相應的模塊加載器代碼的。
SimpleModule.ts
import m = require("mod");
export let t = m.something + 1;
AMD / RequireJS SimpleModule.js
define(["require", "exports", "./mod"], function (require, exports, mod_1) {
exports.t = mod_1.something + 1;
});
CommonJS / Node SimpleModule.js
let mod_1 = require("./mod");
exports.t = mod_1.something + 1;
UMD SimpleModule.js
(function (factory) {
if (typeof module === "object" && typeof module.exports === "object") {
let v = factory(require, exports); if (v !== undefined) module.exports = v;
}
else if (typeof define === "function" && define.amd) {
define(["require", "exports", "./mod"], factory);
}
})(function (require, exports) {
let mod_1 = require("./mod");
exports.t = mod_1.something + 1;
});
System SimpleModule.js
System.register(["./mod"], function(exports_1) {
let mod_1;
let t;
return {
setters:[
function (mod_1_1) {
mod_1 = mod_1_1;
}],
execute: function() {
exports_1("t", t = mod_1.something + 1);
}
}
});
Native ECMAScript 2015 modules SimpleModule.js
import { something } from "./mod";
export let t = something + 1;
簡單示例
下面咱們來整理一下前面的驗證器實現,每一個模塊只有一個命名的導出。
爲了編譯,咱們必須要在命令行上指定一個模塊目標。對於Node.js來講,使用--module commonjs; 對於Require.js來講,使用`–module amd
。好比:
tsc --module commonjs Test.ts
編譯完成後,每一個模塊會生成一個單獨的.js文件。 比如使用了reference標籤,編譯器會根據import語句編譯相應的文件。
Validation.ts
export interface StringValidator {
isAcceptable(s: string): boolean;
}
LettersOnlyValidator.ts
import { StringValidator } from "./Validation";
const lettersRegexp = /^[A-Za-z]+$/;
export class LettersOnlyValidator implements StringValidator {
isAcceptable(s: string) {
return lettersRegexp.test(s);
}
}
ZipCodeValidator.ts
import { StringValidator } from "./Validation";
const numberRegexp = /^[0-9]+$/;
export class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
Test.ts
import { StringValidator } from "./Validation";
import { ZipCodeValidator } from "./ZipCodeValidator";
import { LettersOnlyValidator } from "./LettersOnlyValidator";
// Some samples to try
let strings = ["Hello", "98052", "101"];
// Validators to use
let validators: { [s: string]: StringValidator; } = {};
validators["ZIP code"] = new ZipCodeValidator();
validators["Letters only"] = new LettersOnlyValidator();
// Show whether each string passed each validator
strings.forEach(s => {
for (let name in validators) {
console.log("${ s }" - ${ validators[name].isAcceptable(s) ? "matches" : "does not match" } ${ name }
);
}
});
可選的模塊加載和其它高級加載場景
有時候,你只想在某種條件下才加載某個模塊。 在TypeScript裏,使用下面的方式來實現它和其它的高級加載場景,咱們能夠直接調用模塊加載器而且能夠保證類型徹底。
編譯器會檢測是否每一個模塊都會在生成的JavaScript中用到。 若是一個模塊標識符只在類型註解部分使用,而且徹底沒有在表達式中使用時,就不會生成require這個模塊的代碼。 省略掉沒有用到的引用對性能提高是頗有益的,並同時提供了選擇性加載模塊的能力。
這種模式的核心是import id = require("...")語句可讓咱們訪問模塊導出的類型。 模塊加載器會被動態調用(經過require),就像下面if代碼塊裏那樣。 它利用了省略引用的優化,因此模塊只在被須要時加載。 爲了讓這個模塊工做,必定要注意import定義的標識符只能在表示類型處使用(不能在會轉換成JavaScript的地方)。
爲了確保類型安全性,咱們能夠使用typeof關鍵字。 typeof關鍵字,當在表示類型的地方使用時,會得出一個類型值,這裏就表示模塊的類型。
示例:Node.js裏的動態模塊加載
declare function require(moduleName: string): any;
import { ZipCodeValidator as Zip } from "./ZipCodeValidator";
if (needZipValidation) {
let ZipCodeValidator: typeof Zip = require("./ZipCodeValidator");
let validator = new ZipCodeValidator();
if (validator.isAcceptable("...")) { / ... / }
}
示例:require.js裏的動態模塊加載
declare function require(moduleNames: string[], onLoad: (...args: any[]) => void): void;
import * as Zip from "./ZipCodeValidator";
if (needZipValidation) {
require(["./ZipCodeValidator"], (ZipCodeValidator: typeof Zip) => {
let validator = new ZipCodeValidator.ZipCodeValidator();
if (validator.isAcceptable("...")) { / ... / }
});
}
示例:System.js裏的動態模塊加載
declare const System: any;
import { ZipCodeValidator as Zip } from "./ZipCodeValidator";
if (needZipValidation) {
System.import("./ZipCodeValidator").then((ZipCodeValidator: typeof Zip) => {
var x = new ZipCodeValidator();
if (x.isAcceptable("...")) { / ... / }
});
}
使用其它的JavaScript庫
要想描述非TypeScript編寫的類庫的類型,咱們須要聲明類庫所暴露出的API。
咱們叫它聲明由於它不是「外部程序」的具體實現。 它們一般是在.d.ts文件裏定義的。 若是你熟悉C/C++,你能夠把它們當作.h文件。 讓咱們看一些例子。
外部模塊
在Node.js裏大部分工做是經過加載一個或多個模塊實現的。 咱們能夠使用頂級的export聲明來爲每一個模塊都定義一個.d.ts文件,但最好仍是寫在一個大的.d.ts文件裏。 咱們使用與構造一個外部命名空間類似的方法,可是這裏使用module關鍵字而且把名字用引號括起來,方便以後import。 例如:
node.d.ts (simplified excerpt)
declare module "url" {
export interface Url {
protocol?: string;
hostname?: string;
pathname?: string;
}
export function parse(urlStr: string, parseQueryString?, slashesDenoteHost?): Url;
}
declare module "path" {
export function normalize(p: string): string;
export function join(...paths: any[]): string;
export let sep: string;
}
如今咱們能夠/// <reference> node.d.ts而且使用import url = require("url");或import as URL from "url"加載模塊。
/// <reference path="node.d.ts"/>
import as URL from "url";
let myUrl = URL.parse("http://www.typescriptlang.org");
外部模塊簡寫
假如你不想在使用一個新模塊以前花時間去編寫聲明,你能夠採用聲明的簡寫形式以便可以快速使用它。
declarations.d.ts
declare module "hot-new-module";
簡寫模塊裏全部導出的類型將是any。
import x, {y} from "hot-new-module";
x(y);
模塊聲明通配符
某些模塊加載器如SystemJS 和AMD支持導入非JavaScript內容。 它們一般會使用一個前綴或後綴來表示特殊的加載語法。 模塊聲明通配符能夠用來表示這些狀況。
declare module "!text" {
const content: string;
export default content;
}
// Some do it the other way around.
declare module "json!" {
const value: any;
export default value;
}
如今你能夠就導入匹配"!text"或"json!"的內容了。
import fileContent from "./xyz.txt!text";
import data from "json!http://example.com/data.json";
console.log(data, fileContent);
UMD模塊
有些模塊被設計成兼容多個模塊加載器,或者不使用模塊加載器(全局變量)。 它們以UMD或Isomorphic模塊爲表明。 這些庫能夠經過導入的形式或全局變量的形式訪問。 例如:
math-lib.d.ts
export function isPrime(x: number): boolean;
export as namespace mathLib;
以後,這個庫能夠在某個模塊裏經過導入來使用:
import { isPrime } from "math-lib";
isPrime(2);
mathLib.isPrime(2); // ERROR: can't use the global definition from inside a module
它一樣能夠經過全局變量的形式使用,但只能在某個腳本里。 (腳本是指一個不帶有導入或導出的文件。)
mathLib.isPrime(2);
建立模塊結構指導
儘量地在頂層導出
用戶應該更容易地使用你模塊導出的內容。 嵌套層次過多會變得難以處理,所以仔細考慮一下如何組織你的代碼。
從你的模塊中導出一個命名空間就是一個增長嵌套的例子。 雖然命名空間有時候有它們的用處,在使用模塊的時候它們額外地增長了一層。 這對用戶來講是很不便的而且一般是多餘的。
導出類的靜態方法也有一樣的問題 - 這個類自己就增長了一層嵌套。 除非它能方便表述或便於清晰使用,不然請考慮直接導出一個輔助方法。
若是僅導出單個 class 或 function,使用 export default
就像「在頂層上導出」幫助減小用戶使用的難度,一個默認的導出也能起到這個效果。 若是一個模塊就是爲了導出特定的內容,那麼你應該考慮使用一個默認導出。 這會令模塊的導入和使用變得些許簡單。 好比:
MyClass.ts
export default class SomeType {
constructor() { ... }
}
MyFunc.ts
export default function getThing() { return 'thing'; }
Consumer.ts
import t from "./MyClass";
import f from "./MyFunc";
let x = new t();
console.log(f());
對用戶來講這是最理想的。他們能夠隨意命名導入模塊的類型(本例爲t)而且不須要多餘的(.)來找到相關對象。
若是要導出多個對象,把它們放在頂層裏導出
MyThings.ts
export class SomeType { / ... / }
export function someFunc() { / ... / }
相反地,當導入的時候:
明確地列出導入的名字
Consumer.ts
import { SomeType, SomeFunc } from "./MyThings";
let x = new SomeType();
let y = someFunc();
使用命名空間導入模式當你要導出大量內容的時候
MyLargeModule.ts
export class Dog { ... }
export class Cat { ... }
export class Tree { ... }
export class Flower { ... }
Consumer.ts
import * as myLargeModule from "./MyLargeModule.ts";
let x = new myLargeModule.Dog();
使用從新導出進行擴展
你可能常常須要去擴展一個模塊的功能。 JS裏經常使用的一個模式是JQuery那樣去擴展原對象。 如咱們以前提到的,模塊不會像全局命名空間對象那樣去合併。 推薦的方案是不要去改變原來的對象,而是導出一個新的實體來提供新的功能。
假設Calculator.ts模塊裏定義了一個簡單的計算器實現。 這個模塊一樣提供了一個輔助函數來測試計算器的功能,經過傳入一系列輸入的字符串並在最後給出結果。
Calculator.ts
export class Calculator {
private current = 0;
private memory = 0;
private operator: string;
protected processDigit(digit: string, currentValue: number) { if (digit >= "0" && digit <= "9") { return currentValue * 10 + (digit.charCodeAt(0) - "0".charCodeAt(0)); } } protected processOperator(operator: string) { if (["+", "-", "*", "/"].indexOf(operator) >= 0) { return operator; } } protected evaluateOperator(operator: string, left: number, right: number): number { switch (this.operator) { case "+": return left + right; case "-": return left - right; case "*": return left * right; case "/": return left / right; } } private evaluate() { if (this.operator) { this.memory = this.evaluateOperator(this.operator, this.memory, this.current); } else { this.memory = this.current; } this.current = 0; } public handelChar(char: string) { if (char === "=") { this.evaluate(); return; } else { let value = this.processDigit(char, this.current); if (value !== undefined) { this.current = value; return; } else { let value = this.processOperator(char); if (value !== undefined) { this.evaluate(); this.operator = value; return; } } } throw new Error(`Unsupported input: '${char}'`); } public getResult() { return this.memory; }
}
export function test(c: Calculator, input: string) {
for (let i = 0; i < input.length; i++) {
c.handelChar(input[i]);
}
console.log(`result of '${input}' is '${c.getResult()}'`);
}
下面使用導出的test函數來測試計算器。
TestCalculator.ts
import { Calculator, test } from "./Calculator";
let c = new Calculator();
test(c, "1+2*33/11="); // prints 9
如今擴展它,添加支持輸入其它進制(十進制之外),讓咱們來建立ProgrammerCalculator.ts。
ProgrammerCalculator.ts
import { Calculator } from "./Calculator";
class ProgrammerCalculator extends Calculator {
static digits = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"];
constructor(public base: number) { super(); if (base <= 0 || base > ProgrammerCalculator.digits.length) { throw new Error("base has to be within 0 to 16 inclusive."); } } protected processDigit(digit: string, currentValue: number) { if (ProgrammerCalculator.digits.indexOf(digit) >= 0) { return currentValue * this.base + ProgrammerCalculator.digits.indexOf(digit); } }
}
// Export the new extended calculator as Calculator
export { ProgrammerCalculator as Calculator };
// Also, export the helper function
export { test } from "./Calculator";
新的ProgrammerCalculator模塊導出的API與原先的Calculator模塊很類似,但卻沒有改變原模塊裏的對象。 下面是測試ProgrammerCalculator類的代碼:
TestProgrammerCalculator.ts
import { Calculator, test } from "./ProgrammerCalculator";
let c = new Calculator(2);
test(c, "001+010="); // prints 3
模塊裏不要使用命名空間
當初次進入基於模塊的開發模式時,可能總會控制不住要將導出包裹在一個命名空間裏。 模塊具備其本身的做用域,而且只有導出的聲明纔會在模塊外部可見。 記住這點,命名空間在使用模塊時幾乎沒什麼價值。
在組織方面,命名空間對於在全局做用域內對邏輯上相關的對象和類型進行分組是很便利的。 例如,在C#裏,你會從System.Collections裏找到全部集合的類型。 經過將類型有層次地組織在命名空間裏,能夠方便用戶找到與使用那些類型。 然而,模塊自己已經存在於文件系統之中,這是必須的。 咱們必須經過路徑和文件名找到它們,這已經提供了一種邏輯上的組織形式。 咱們能夠建立/collections/generic/文件夾,把相應模塊放在這裏面。
命名空間對解決全局做用域裏命名衝突來講是很重要的。 好比,你能夠有一個My.Application.Customer.AddForm和My.Application.Order.AddForm – 兩個類型的名字相同,但命名空間不一樣。 然而,這對於模塊來講卻不是一個問題。 在一個模塊裏,沒有理由兩個對象擁有同一個名字。 從模塊的使用角度來講,使用者會挑出他們用來引用模塊的名字,因此也沒有理由發生重名的狀況。
命名空間
第一步
咱們先來寫一段程序並將在整篇文章中都使用這個例子。 咱們定義幾個簡單的字符串驗證器,假設你會使用它們來驗證表單裏的用戶輸入或驗證外部數據。
全部的驗證器都放在一個文件裏
interface StringValidator {
isAcceptable(s: string): boolean;
}
let lettersRegexp = /^[A-Za-z]+$/;
let numberRegexp = /^[0-9]+$/;
class LettersOnlyValidator implements StringValidator {
isAcceptable(s: string) {
return lettersRegexp.test(s);
}
}
class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
// Some samples to try
let strings = ["Hello", "98052", "101"];
// Validators to use
let validators: { [s: string]: StringValidator; } = {};
validators["ZIP code"] = new ZipCodeValidator();
validators["Letters only"] = new LettersOnlyValidator();
// Show whether each string passed each validator
for (let s of strings) {
for (let name in validators) {
let isMatch = validators[name].isAcceptable(s);
console.log('${ s }' ${ isMatch ? "matches" : "does not match" } '${ name }'.
);
}
}
命名空間
隨着更多驗證器的加入,咱們須要一種手段來組織代碼,以便於在記錄它們類型的同時還不用擔憂與其它對象產生命名衝突。 所以,咱們把驗證器包裹到一個命名空間內,而不是把它們放在全局命名空間下。
下面的例子裏,把全部與驗證器相關的類型都放到一個叫作Validation的命名空間裏。 由於咱們想讓這些接口和類在命名空間以外也是可訪問的,因此須要使用export。 相反的,變量lettersRegexp和numberRegexp是實現的細節,不須要導出,所以它們在命名空間外是不能訪問的。 在文件末尾的測試代碼裏,因爲是在命名空間以外訪問,所以須要限定類型的名稱,好比Validation.LettersOnlyValidator。
使用命名空間的驗證器
namespace Validation {
export interface StringValidator {
isAcceptable(s: string): boolean;
}
const lettersRegexp = /^[A-Za-z]+$/; const numberRegexp = /^[0-9]+$/; export class LettersOnlyValidator implements StringValidator { isAcceptable(s: string) { return lettersRegexp.test(s); } } export class ZipCodeValidator implements StringValidator { isAcceptable(s: string) { return s.length === 5 && numberRegexp.test(s); } }
}
// Some samples to try
let strings = ["Hello", "98052", "101"];
// Validators to use
let validators: { [s: string]: Validation.StringValidator; } = {};
validators["ZIP code"] = new Validation.ZipCodeValidator();
validators["Letters only"] = new Validation.LettersOnlyValidator();
// Show whether each string passed each validator
for (let s of strings) {
for (let name in validators) {
console.log("${ s }" - ${ validators[name].isAcceptable(s) ? "matches" : "does not match" } ${ name }
);
}
}
分離到多文件
當應用變得愈來愈大時,咱們須要將代碼分離到不一樣的文件中以便於維護。
多文件中的命名空間
如今,咱們把Validation命名空間分割成多個文件。 儘管是不一樣的文件,它們還是同一個命名空間,而且在使用的時候就如同它們在一個文件中定義的同樣。 由於不一樣文件之間存在依賴關係,因此咱們加入了引用標籤來告訴編譯器文件之間的關聯。 咱們的測試代碼保持不變。
Validation.ts
namespace Validation {
export interface StringValidator {
isAcceptable(s: string): boolean;
}
}
LettersOnlyValidator.ts
/// <reference path="Validation.ts" />
namespace Validation {
const lettersRegexp = /^[A-Za-z]+$/;
export class LettersOnlyValidator implements StringValidator {
isAcceptable(s: string) {
return lettersRegexp.test(s);
}
}
}
ZipCodeValidator.ts
/// <reference path="Validation.ts" />
namespace Validation {
const numberRegexp = /^[0-9]+$/;
export class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
}
Test.ts
/// <reference path="Validation.ts" />
/// <reference path="LettersOnlyValidator.ts" />
/// <reference path="ZipCodeValidator.ts" />
// Some samples to try
let strings = ["Hello", "98052", "101"];
// Validators to use
let validators: { [s: string]: Validation.StringValidator; } = {};
validators["ZIP code"] = new Validation.ZipCodeValidator();
validators["Letters only"] = new Validation.LettersOnlyValidator();
// Show whether each string passed each validator
for (let s of strings) {
for (let name in validators) {
console.log(""" + s + "" " + (validators[name].isAcceptable(s) ? " matches " : " does not match ") + name);
}
}
當涉及到多文件時,咱們必須確保全部編譯後的代碼都被加載了。 咱們有兩種方式。
第一種方式,把全部的輸入文件編譯爲一個輸出文件,須要使用--outFile標記:
tsc --outFile sample.js Test.ts
編譯器會根據源碼裏的引用標籤自動地對輸出進行排序。你也能夠單獨地指定每一個文件。
tsc --outFile sample.js Validation.ts LettersOnlyValidator.ts ZipCodeValidator.ts Test.ts
第二種方式,咱們能夠編譯每個文件(默認方式),那麼每一個源文件都會對應生成一個JavaScript文件。 而後,在頁面上經過<script>標籤把全部生成的JavaScript文件按正確的順序引進來,好比:
MyTestPage.html (excerpt)
<script src="Validation.js" type="text/javascript" />
<script src="LettersOnlyValidator.js" type="text/javascript" />
<script src="ZipCodeValidator.js" type="text/javascript" />
<script src="Test.js" type="text/javascript" />
別名
另外一種簡化命名空間操做的方法是使用import q = x.y.z給經常使用的對象起一個短的名字。 不要與用來加載模塊的import x = require('name')語法弄混了,這裏的語法是爲指定的符號建立一個別名。 你能夠用這種方法爲任意標識符建立別名,也包括導入的模塊中的對象。
namespace Shapes {
export namespace Polygons {
export class Triangle { }
export class Square { }
}
}
import polygons = Shapes.Polygons;
let sq = new polygons.Square(); // Same as "new Shapes.Polygons.Square()"
注意,咱們並無使用require關鍵字,而是直接使用導入符號的限定名賦值。 這與使用var類似,但它還適用於類型和導入的具備命名空間含義的符號。 重要的是,對於值來說,import會生成與原始符號不一樣的引用,因此改變別名的var值並不會影響原始變量的值。
使用其它的JavaScript庫
爲了描述不是用TypeScript編寫的類庫的類型,咱們須要聲明類庫導出的API。 因爲大部分程序庫只提供少數的頂級對象,命名空間是用來表示它們的一個好辦法。
咱們稱其爲聲明是由於它不是外部程序的具體實現。 咱們一般在.d.ts裏寫這些聲明。 若是你熟悉C/C++,你能夠把它們當作.h文件。 讓咱們看一些例子。
外部命名空間
流行的程序庫D3在全局對象d3裏定義它的功能。 由於這個庫經過一個<script>標籤加載(不是經過模塊加載器),它的聲明文件使用內部模塊來定義它的類型。 爲了讓TypeScript編譯器識別它的類型,咱們使用外部命名空間聲明。 好比,咱們能夠像下面這樣寫:
D3.d.ts (部分摘錄)
declare namespace D3 {
export interface Selectors {
select: {
(selector: string): Selection;
(element: EventTarget): Selection;
};
}
export interface Event { x: number; y: number; } export interface Base extends Selectors { event: Event; }
}
命名空間和模塊
和模塊的高級使用場景,和在使用它們的過程當中常見的陷阱。
查看模塊章節瞭解關於模塊的更多信息。 查看命名空間章節瞭解關於命名空間的更多信息。
使用命名空間
命名空間是位於全局命名空間下的一個普通的帶有名字的JavaScript對象。 這令命名空間十分容易使用。 它們能夠在多文件中同時使用,並經過--outFile結合在一塊兒。 命名空間是幫你組織Web應用不錯的方式,你能夠把全部依賴都放在HTML頁面的<script>標籤裏。
但就像其它的全局命名空間污染同樣,它很難去識別組件之間的依賴關係,尤爲是在大型的應用中。
使用模塊
像命名空間同樣,模塊能夠包含代碼和聲明。 不一樣的是模塊能夠聲明它的依賴。
模塊會把依賴添加到模塊加載器上(例如CommonJs / Require.js)。 對於小型的JS應用來講可能不必,可是對於大型應用,這一點點的花費會帶來長久的模塊化和可維護性上的便利。 模塊也提供了更好的代碼重用,更強的封閉性以及更好的使用工具進行優化。
對於Node.js應用來講,模塊是默認並推薦的組織代碼的方式。
從ECMAScript 2015開始,模塊成爲了語言內置的部分,應該會被全部正常的解釋引擎所支持。 所以,對於新項目來講推薦使用模塊作爲組織代碼的方式。
命名空間和模塊的陷阱
這部分咱們會描述常見的命名空間和模塊的使用陷阱和如何去避免它們。
對模塊使用/// <reference>
一個常見的錯誤是使用/// <reference>引用模塊文件,應該使用import。 要理解這之間的區別,咱們首先應該弄清編譯器是如何根據import路徑(例如,import x from "...";或import x = require("...")裏面的...,等等)來定位模塊的類型信息的。
編譯器首先嚐試去查找相應路徑下的.ts,.tsx再或者.d.ts。 若是這些文件都找不到,編譯器會查找外部模塊聲明。 回想一下,它們是在.d.ts文件裏聲明的。
myModules.d.ts
// In a .d.ts file or .ts file that is not a module:
declare module "SomeModule" {
export function fn(): string;
}
myOtherModule.ts
/// <reference path="myModules.d.ts" />
import as m from "SomeModule";
這裏的引用標籤指定了外來模塊的位置。 這就是一些TypeScript例子中引用node.d.ts的方法。
沒必要要的命名空間
若是你想把命名空間轉換爲模塊,它可能會像下面這個文件一件:
shapes.ts
export namespace Shapes {
export class Triangle { / ... / }
export class Square { / ... / }
}
頂層的模塊Shapes包裹了Triangle和Square。 對於使用它的人來講這是使人迷惑和討厭的:
shapeConsumer.ts
import as shapes from "./shapes";
let t = new shapes.Shapes.Triangle(); // shapes.Shapes?
TypeScript裏模塊的一個特色是不一樣的模塊永遠也不會在相同的做用域內使用相同的名字。 由於使用模塊的人會爲它們命名,因此徹底沒有必要把導出的符號包裹在一個命名空間裏。
再次重申,不該該對模塊使用命名空間,使用命名空間是爲了提供邏輯分組和避免命名衝突。 模塊文件自己已是一個邏輯分組,而且它的名字是由導入這個模塊的代碼指定,因此沒有必要爲導出的對象增長額外的模塊層。
下面是改進的例子:
shapes.ts
export class Triangle { / ... / }
export class Square { / ... / }
shapeConsumer.ts
import * as shapes from "./shapes";
let t = new shapes.Triangle();
模塊的取捨
就像每一個JS文件對應一個模塊同樣,TypeScript裏模塊文件與生成的JS文件也是一一對應的。 這會產生一種影響,根據你指定的目標模塊系統的不一樣,你可能沒法鏈接多個模塊源文件。 例如當目標模塊系統爲commonjs或umd時,沒法使用outFile選項,可是在TypeScript 1.8以上的版本可以使用outFile當目標爲amd或system
模塊解析
模塊解析就是指編譯器所要依據的一個流程,用它來找出某個導入操做所引用的具體值。 假設有一個導入語句import { a } from "moduleA"; 爲了去檢查任何對a的使用,編譯器須要準確的知道它表示什麼,而且會須要檢查它的定義moduleA。
這時候,編譯器會想知道「moduleA的shape是怎樣的?」 這聽上去很簡單,moduleA可能在你寫的某個.ts/.tsx文件裏或者在你的代碼所依賴的.d.ts裏。
首先,編譯器會嘗試定位表示導入模塊的文件。 編譯會遵循下列二種策略之一:Classic或Node。 這些策略會告訴編譯器到哪裏去查找moduleA。
若是它們失敗了而且若是模塊名是非相對的(且是在"moduleA"的狀況下),編譯器會嘗試定位一個外部模塊聲明。 咱們接下來會講到非相對導入。
最後,若是編譯器仍是不能解析這個模塊,它會記錄一個錯誤。 在這種狀況下,錯誤可能爲error TS2307: Cannot find module 'moduleA'.
相對 vs. 非相對模塊導入
根據模塊引用是相對的仍是非相對的,模塊導入會以不一樣的方式解析。
相對導入是以/,./或../開頭的。 下面是一些例子:
import Entry from "./components/Entry";
import { DefaultHeaders } from "../constants/http";
import "/mod";
全部其它形式的導入被看成非相對的。 下面是一些例子:
import * as $ from "jQuery";
import { Component } from "@angular/core";
相對導入解析時是相對於導入它的文件來的,而且不能解析爲一個外部模塊聲明。 你應該爲你本身寫的模塊使用相對導入,這樣能確保它們在運行時的相對位置。
非相對模塊的導入能夠相對於baseUrl或經過下文會講到的路徑映射來進行解析。 它們還能夠被解析能外部模塊聲明。 使用非相對路徑來導入你的外部依賴。
模塊解析策略
共有兩種可用的模塊解析策略:Node和Classic。 你能夠使用--moduleResolution標記指定使用哪一種模塊解析策略。 若未指定,那麼在使用了--module AMD | System | ES2015時的默認值爲Classic,其它狀況時則爲Node。
Classic
這種策略之前是TypeScript默認的解析策略。 如今,它存在的理由主要是爲了向後兼容。
相對導入的模塊是相對於導入它的文件進行解析的。 所以/root/src/folder/A.ts文件裏的import { b } from "./moduleB"會使用下面的查找流程:
/root/src/folder/moduleB.ts
/root/src/folder/moduleB.d.ts
對於非相對模塊的導入,編譯器則會從包含導入文件的目錄開始依次向上級目錄遍歷,嘗試定位匹配的聲明文件。
好比:
有一個對moduleB的非相對導入import { b } from "moduleB",它是在/root/src/folder/A.ts文件裏,會以以下的方式來定位"moduleB":
/root/src/folder/moduleB.ts
/root/src/folder/moduleB.d.ts
/root/src/moduleB.ts
/root/src/moduleB.d.ts
/root/moduleB.ts
/root/moduleB.d.ts
/moduleB.ts
/moduleB.d.ts
Node
這個解析策略試圖在運行時模仿Node.js模塊解析機制。 完整的Node.js解析算法能夠在Node.js module documentation找到。
Node.js如何解析模塊
爲了理解TypeScript編譯依照的解析步驟,先弄明白Node.js模塊是很是重要的。 一般,在Node.js裏導入是經過require函數調用進行的。 Node.js會根據require的是相對路徑仍是非相對路徑作出不一樣的行爲。
相對路徑很簡單。 例如,假設有一個文件路徑爲/root/src/moduleA.js,包含了一個導入var x = require("./moduleB"); Node.js如下面的順序解析這個導入:
將/root/src/moduleB.js視爲文件,檢查是否存在。
將/root/src/moduleB視爲目錄,檢查是否它包含package.json文件而且其指定了一個"main"模塊。 在咱們的例子裏,若是Node.js發現文件/root/src/moduleB/package.json包含了{ "main": "lib/mainModule.js" },那麼Node.js會引用/root/src/moduleB/lib/mainModule.js。
將/root/src/moduleB視爲目錄,檢查它是否包含index.js文件。 這個文件會被隱式地看成那個文件夾下的」main」模塊。
你能夠閱讀Node.js文檔瞭解更多詳細信息:file modules 和 folder modules。
可是,非相對模塊名的解析是個徹底不一樣的過程。 Node會在一個特殊的文件夾node_modules裏查找你的模塊。 node_modules可能與當前文件在同一級目錄下,或者在上層目錄裏。 Node會向上級目錄遍歷,查找每一個node_modules直到它找到要加載的模塊。
仍是用上面例子,但假設/root/src/moduleA.js裏使用的是非相對路徑導入var x = require("moduleB");。 Node則會如下面的順序去解析moduleB,直到有一個匹配上。
/root/src/node_modules/moduleB.js
/root/src/node_modules/moduleB/package.json (若是指定了"main"屬性)
/root/src/node_modules/moduleB/index.js
/root/node_modules/moduleB.js
/root/node_modules/moduleB/package.json (若是指定了"main"屬性)
/root/node_modules/moduleB/index.js
/node_modules/moduleB.js
/node_modules/moduleB/package.json (若是指定了"main"屬性)
/node_modules/moduleB/index.js
注意Node.js在步驟(4)和(7)會向上跳一級目錄。
你能夠閱讀Node.js文檔瞭解更多詳細信息:loading modules from node_modules。
TypeScript如何解析模塊
TypeScript是模仿Node.js運行時的解析策略來在編譯階段定位模塊定義文件。 所以,TypeScript在Node解析邏輯基礎上增長了TypeScript源文件的擴展名(.ts,.tsx和.d.ts)。 同時,TypeScript在package.json裏使用字段"types"來表示相似"main"的意義 - 編譯器會使用它來找到要使用的」main」定義文件。
好比,有一個導入語句import { b } from "./moduleB"在/root/src/moduleA.ts裏,會如下面的流程來定位"./moduleB":
/root/src/moduleB.ts
/root/src/moduleB.tsx
/root/src/moduleB.d.ts
/root/src/moduleB/package.json (若是指定了"types"屬性)
/root/src/moduleB/index.ts
/root/src/moduleB/index.tsx
/root/src/moduleB/index.d.ts
回想一下Node.js先查找moduleB.js文件,而後是合適的package.json,再以後是index.js。
相似地,非相對的導入會遵循Node.js的解析邏輯,首先查找文件,而後是合適的文件夾。 所以/root/src/moduleA.ts文件裏的import { b } from "moduleB"會如下面的查找順序解析:
/root/src/node_modules/moduleB.ts
/root/src/node_modules/moduleB.tsx
/root/src/node_modules/moduleB.d.ts
/root/src/node_modules/moduleB/package.json (若是指定了"types"屬性)
/root/src/node_modules/moduleB/index.ts
/root/src/node_modules/moduleB/index.tsx
/root/src/node_modules/moduleB/index.d.ts
/root/node_modules/moduleB.ts
/root/node_modules/moduleB.tsx
/root/node_modules/moduleB.d.ts
/root/node_modules/moduleB/package.json (若是指定了"types"屬性)
/root/node_modules/moduleB/index.ts
/root/node_modules/moduleB/index.tsx
/root/node_modules/moduleB/index.d.ts
/node_modules/moduleB.ts
/node_modules/moduleB.tsx
/node_modules/moduleB.d.ts
/node_modules/moduleB/package.json (若是指定了"types"屬性)
/node_modules/moduleB/index.ts
/node_modules/moduleB/index.tsx
/node_modules/moduleB/index.d.ts
不要被這裏步驟的數量嚇到 - TypeScript只是在步驟(8)和(15)向上跳了兩次目錄。 這並不比Node.js裏的流程複雜。
附加的模塊解析標記
有時工程源碼結構與輸出結構不一樣。 一般是要通過一系統的構建步驟最後生成輸出。 它們包括將.ts編譯成.js,將不一樣位置的依賴拷貝至一個輸出位置。 最終結果就是運行時的模塊名與包含它們聲明的源文件裏的模塊名不一樣。 或者最終輸出文件裏的模塊路徑與編譯時的源文件路徑不一樣了。
TypeScript編譯器有一些額外的標記用來通知編譯器在源碼編譯成最終輸出的過程當中都發生了哪一個轉換。
有一點要特別注意的是編譯器不會進行這些轉換操做; 它只是利用這些信息來指導模塊的導入。
Base URL
在利用AMD模塊加載器的應用裏使用baseUrl是常見作法,它要求在運行時模塊都被放到了一個文件夾裏。 這些模塊的源碼能夠在不一樣的目錄下,可是構建腳本會將它們集中到一塊兒。
設置baseUrl來告訴編譯器到哪裏去查找模塊。 全部非相對模塊導入都會被當作相對於baseUrl。
baseUrl的值由如下二者之一決定:
命令行中baseUrl的值(若是給定的路徑是相對的,那麼將相對於當前路徑進行計算)
‘tsconfig.json’裏的baseUrl屬性(若是給定的路徑是相對的,那麼將相對於‘tsconfig.json’路徑進行計算)
注意相對模塊的導入不會被設置的baseUrl所影響,由於它們老是相對於導入它們的文件。
閱讀更多關於baseUrl的信息RequireJS和SystemJS。
路徑映射
有時模塊不是直接放在baseUrl下面。 好比,充分"jquery"模塊地導入,在運行時可能被解釋爲"node_modules/jquery/dist/jquery.slim.min.js"。 加載器使用映射配置來將模塊名映射到運行時的文件,查看RequireJs documentation和SystemJS documentation。
TypeScript編譯器經過使用tsconfig.json文件裏的"paths"來支持這樣的聲明映射。 下面是一個如何指定jquery的"paths"的例子。
{
"compilerOptions": {
"baseUrl": ".", // This must be specified if "paths" is.
"paths": {
"jquery": ["node_modules/jquery/dist/jquery"] // 此處映射是相對於"baseUrl"
}
}
}
請注意"paths"是相對於"baseUrl"進行解析。 若是"baseUrl"被設置成了除"."外的其它值,好比tsconfig.json所在的目錄,那麼映射必需要作相應的改變。 若是你在上例中設置了"baseUrl": "./src",那麼jquery應該映射到"../node_modules/jquery/dist/jquery"。
經過"paths"咱們還能夠指定複雜的映射,包括指定多個回退位置。 假設在一個工程配置裏,有一些模塊位於一處,而其它的則在另個的位置。 構建過程會將它們集中至一處。 工程結構可能以下:
projectRoot
├── folder1
│ ├── file1.ts (imports 'folder1/file2' and 'folder2/file3')
│ └── file2.ts
├── generated
│ ├── folder1
│ └── folder2
│ └── file3.ts
└── tsconfig.json
相應的tsconfig.json文件以下:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"": [
"",
"generated/"
]
}
}
}
它告訴編譯器全部匹配""(全部的值)模式的模塊導入會在如下兩個位置查找:
"": 表示名字不發生改變,因此映射爲<moduleName> => <baseUrl>/<moduleName>
"generated/"表示模塊名添加了「generated」前綴,因此映射爲<moduleName> => <baseUrl>/generated/<moduleName>
按照這個邏輯,編譯器將會以下嘗試解析這兩個導入:
導入’folder1/file2’
匹配’‘模式且通配符捕獲到整個名字。
嘗試列表裏的第一個替換:’’ -> folder1/file2。
替換結果爲非相對名 - 與baseUrl合併 -> projectRoot/folder1/file2.ts。
文件存在。完成。
導入’folder2/file3’
匹配’‘模式且通配符捕獲到整個名字。
嘗試列表裏的第一個替換:’’ -> folder2/file3。
替換結果爲非相對名 - 與baseUrl合併 -> projectRoot/folder2/file3.ts。
文件不存在,跳到第二個替換。
第二個替換:’generated/*’ -> generated/folder2/file3。
替換結果爲非相對名 - 與baseUrl合併 -> projectRoot/generated/folder2/file3.ts。
文件存在。完成。
利用rootDirs指定虛擬目錄
有時多個目錄下的工程源文件在編譯時會進行合併放在某個輸出目錄下。 這能夠看作一些源目錄建立了一個「虛擬」目錄。
利用rootDirs,能夠告訴編譯器生成這個虛擬目錄的roots; 所以編譯器能夠在「虛擬」目錄下解析相對模塊導入,就好像它們被合併在了一塊兒同樣。
好比,有下面的工程結構:
src
└── views
└── view1.ts (imports './template1')
└── view2.ts
generated
└── templates
└── views
└── template1.ts (imports './view2')
src/views裏的文件是用於控制UI的用戶代碼。 generated/templates是UI模版,在構建時經過模版生成器自動生成。 構建中的一步會將/src/views和/generated/templates/views的輸出拷貝到同一個目錄下。 在運行時,視圖能夠假設它的模版與它同在一個目錄下,所以能夠使用相對導入"./template"。
能夠使用"rootDirs"來告訴編譯器。 "rootDirs"指定了一個roots列表,列表裏的內容會在運行時被合併。 所以,針對這個例子,tsconfig.json以下:
{
"compilerOptions": {
"rootDirs": [
"src/views",
"generated/templates/views"
]
}
}
每當編譯器在某一rootDirs的子目錄下發現了相對模塊導入,它就會嘗試從每個rootDirs中導入。
rootDirs的靈活性不只僅侷限於其指定了要在邏輯上合併的物理目錄列表。它提供的數組能夠包含任意數量的任何名字的目錄,不論它們是否存在。這容許編譯器以類型安全的方式處理複雜捆綁(bundles)和運行時的特性,好比條件引入和工程特定的加載器插件。
設想這樣一個國際化的場景,構建工具自動插入特定的路徑記號來生成針對不一樣區域的捆綁,好比將#{locale}作爲相對模塊路徑./#{locale}/messages的一部分。在這個假定的設置下,工具會枚舉支持的區域,將抽像的路徑映射成./zh/messages,./de/messages等。
假設每一個模塊都會導出一個字符串的數組。好比./zh/messages可能包含:
export default [
"您好嗎",
"很高興認識你"
];
利用rootDirs咱們可讓編譯器瞭解這個映射關係,從而也容許編譯器可以安全地解析./#{locale}/messages,就算這個目錄永遠都不存在。好比,使用下面的tsconfig.json:
{
"compilerOptions": {
"rootDirs": [
"src/zh",
"src/de",
"src/#{locale}"
]
}
}
編譯器如今能夠將import messages from './#{locale}/messages'解析爲import messages from './zh/messages'用作工具支持的目的,並容許在開發時沒必要了解區域信息。
跟蹤模塊解析
如以前討論,編譯器在解析模塊時可能訪問當前文件夾外的文件。 這會致使很難診斷模塊爲何沒有被解析,或解析到了錯誤的位置。 經過--traceResolution啓用編譯器的模塊解析跟蹤,它會告訴咱們在模塊解析過程當中發生了什麼。
假設咱們有一個使用了typescript模塊的簡單應用。 app.ts裏有一個這樣的導入import * as ts from "typescript"。
│ tsconfig.json
├───node_modules
│ └───typescript
│ └───lib
│ typescript.d.ts
└───src
app.ts
使用--traceResolution調用編譯器。
tsc --traceResolution
輸出結果以下:
======== Resolving module 'typescript' from 'src/app.ts'. ========
Module resolution kind is not specified, using 'NodeJs'.
Loading module 'typescript' from 'node_modules' folder.
File 'src/node_modules/typescript.ts' does not exist.
File 'src/node_modules/typescript.tsx' does not exist.
File 'src/node_modules/typescript.d.ts' does not exist.
File 'src/node_modules/typescript/package.json' does not exist.
File 'node_modules/typescript.ts' does not exist.
File 'node_modules/typescript.tsx' does not exist.
File 'node_modules/typescript.d.ts' does not exist.
Found 'package.json' at 'node_modules/typescript/package.json'.
'package.json' has 'types' field './lib/typescript.d.ts' that references 'node_modules/typescript/lib/typescript.d.ts'.
File 'node_modules/typescript/lib/typescript.d.ts' exist - use it as a module resolution result.
======== Module name 'typescript' was successfully resolved to 'node_modules/typescript/lib/typescript.d.ts'. ========
須要留意的地方
導入的名字及位置
======== Resolving module ‘typescript’ from ‘src/app.ts’. ========
編譯器使用的策略
Module resolution kind is not specified, using ‘NodeJs’.
從npm加載types
聲明合併
基礎概念
TypeScript中的聲明會建立如下三種實體之一:命名空間,類型或值。 建立命名空間的聲明會新建一個命名空間,它包含了用(.)符號來訪問時使用的名字。 建立類型的聲明是:用聲明的模型建立一個類型並綁定到給定的名字上。 最後,建立值的聲明會建立在JavaScript輸出中看到的值。
Declaration Type
Namespace
Type
Value
Namespace
X
X
Class
X
X
Enum
X
X
Interface
X
Type Alias
X
Function
X
Variable
X
理解每一個聲明建立了什麼,有助於理解當聲明合併時有哪些東西被合併了。
合併接口
最簡單也最多見的聲明合併類型是接口合併。 從根本上說,合併的機制是把雙方的成員放到一個同名的接口裏。
interface Box {
height: number;
width: number;
}
interface Box {
scale: number;
}
let box: Box = {height: 5, width: 6, scale: 10};
接口的非函數的成員應該是惟一的。 若是它們不是惟一的,那麼它們必須是相同的類型。 若是兩個接口中同時聲明瞭同名的非函數成員且它們的類型不一樣,則編譯器會報錯。
對於函數成員,每一個同名函數聲明都會被當成這個函數的一個重載。 同時須要注意,當接口A與後來的接口A合併時,後面的接口具備更高的優先級。
以下例所示:
interface Cloner {
clone(animal: Animal): Animal;
}
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;
}
注意每組接口裏的聲明順序保持不變,但各組接口之間的順序是後來的接口重載出如今靠前位置。
這個規則有一個例外是當出現特殊的函數簽名時。 若是簽名裏有一個參數的類型是單一的字符串字面量(好比,不是字符串字面量的聯合類型),那麼它將會被提高到重載列表的最頂端。
好比,下面的接口會合併到一塊兒:
interface Document {
createElement(tagName: any): Element;
}
interface Document {
createElement(tagName: "div"): HTMLDivElement;
createElement(tagName: "span"): HTMLSpanElement;
}
interface Document {
createElement(tagName: string): HTMLElement;
createElement(tagName: "canvas"): HTMLCanvasElement;
}
合併後的Document將會像下面這樣:
interface Document {
createElement(tagName: "canvas"): HTMLCanvasElement;
createElement(tagName: "div"): HTMLDivElement;
createElement(tagName: "span"): HTMLSpanElement;
createElement(tagName: string): HTMLElement;
createElement(tagName: any): Element;
}
合併命名空間
與接口類似,同名的命名空間也會合並其成員。 命名空間會建立出命名空間和值,咱們須要知道這二者都是怎麼合併的。
對於命名空間的合併,模塊導出的同名接口進行合併,構成單一命名空間內含合併後的接口。
對於命名空間裏值的合併,若是當前已經存在給定名字的命名空間,那麼後來的命名空間的導出成員會被加到已經存在的那個模塊裏。
Animals聲明合併示例:
namespace Animals {
export class Zebra { }
}
namespace Animals {
export interface Legged { numberOfLegs: number; }
export class Dog { }
}
等同於:
namespace Animals {
export interface Legged { numberOfLegs: number; }
export class Zebra { } export class Dog { }
}
除了這些合併外,你還須要瞭解非導出成員是如何處理的。 非導出成員僅在其原有的(合併前的)命名空間內可見。這就是說合並以後,從其它命名空間合併進來的成員沒法訪問非導出成員。
下例提供了更清晰的說明:
namespace Animal {
let haveMuscles = true;
export function animalsHaveMuscles() { return haveMuscles; }
}
namespace Animal {
export function doAnimalsHaveMuscles() {
return haveMuscles; // <-- error, haveMuscles is not visible here
}
}
由於haveMuscles並無導出,只有animalsHaveMuscles函數共享了原始未合併的命名空間能夠訪問這個變量。 doAnimalsHaveMuscles函數雖是合併命名空間的一部分,可是訪問不了未導出的成員。
命名空間與類和函數和枚舉類型合併
命名空間能夠與其它類型的聲明進行合併。 只要命名空間的定義符合將要合併類型的定義。合併結果包含二者的聲明類型。 TypeScript使用這個功能去實現一些JavaScript裏的設計模式。
合併命名空間和類
這讓咱們能夠表示內部類。
class Album {
label: Album.AlbumLabel;
}
namespace Album {
export class AlbumLabel { }
}
合併規則與上面合併命名空間小節裏講的規則一致,咱們必須導出AlbumLabel類,好讓合併的類能訪問。 合併結果是一個類並帶有一個內部類。 你也能夠使用命名空間爲類增長一些靜態屬性。
除了內部類的模式,你在JavaScript裏,建立一個函數稍後擴展它增長一些屬性也是很常見的。 TypeScript使用聲明合併來達到這個目的並保證類型安全。
function buildLabel(name: string): string {
return buildLabel.prefix + name + buildLabel.suffix;
}
namespace buildLabel {
export let suffix = "";
export let prefix = "Hello, ";
}
alert(buildLabel("Sam Smith"));
類似的,命名空間能夠用來擴展枚舉型:
enum Color {
red = 1,
green = 2,
blue = 4
}
namespace Color {
export function mixColor(colorName: string) {
if (colorName == "yellow") {
return Color.red + Color.green;
}
else if (colorName == "white") {
return Color.red + Color.green + Color.blue;
}
else if (colorName == "magenta") {
return Color.red + Color.blue;
}
else if (colorName == "cyan") {
return Color.green + Color.blue;
}
}
}
非法的合併
TypeScript並不是容許全部的合併。 目前,類不能與其它類或變量合併。 想要了解如何模仿類的合併,請參考TypeScript的混入。
模塊擴展
雖然JavaScript不支持合併,但你能夠爲導入的對象打補丁以更新它們。讓咱們考察一下這個玩具性的示例:
// observable.js
export class Observable<T> {
// ... implementation left as an exercise for the reader ...
}
// map.js
import { Observable } from "./observable";
Observable.prototype.map = function (f) {
// ... another exercise for the reader
}
它也能夠很好地工做在TypeScript中, 但編譯器對 Observable.prototype.map一無所知。 你能夠使用擴展模塊來將它告訴編譯器:
// observable.ts stays the same
// map.ts
import { Observable } from "./observable";
declare module "./observable" {
interface Observable<T> {
map<U>(f: (x: T) => U): Observable<U>;
}
}
Observable.prototype.map = function (f) {
// ... another exercise for the reader
}
// consumer.ts
import { Observable } from "./observable";
import "./map";
let o: Observable<number>;
o.map(x => x.toFixed());
模塊名的解析和用import/export解析模塊標識符的方式是一致的。 更多信息請參考 Modules。 當這些聲明在擴展中合併時,就好像在原始位置被聲明瞭同樣。 可是,你不能在擴展中聲明新的頂級聲明-僅能夠擴展模塊中已經存在的聲明。
全局擴展
你也以在模塊內部添加聲明到全局做用域中。
// observable.ts
export class Observable<T> {
// ... still no implementation ...
}
declare global {
interface Array<T> {
toObservable(): Observable<T>;
}
}
Array.prototype.toObservable = function () {
// ...
}
全局擴展與模塊擴展的行爲和限制是相同的。
JSX
介紹
JSX是一種嵌入式的相似XML的語法。 它能夠被轉換成合法的JavaScript,儘管轉換的語義是依據不一樣的實現而定的。 JSX因React框架而流行,可是也被其它應用所使用。 TypeScript支持內嵌,類型檢查和將JSX直接編譯爲JavaScript。
基本用法
想要使用JSX必須作兩件事:
給文件一個.tsx擴展名
啓用jsx選項
TypeScript具備三種JSX模式:preserve,react和react-native。 這些模式只在代碼生成階段起做用 - 類型檢查並不受影響。 在preserve模式下生成代碼中會保留JSX以供後續的轉換操做使用(好比:Babel)。 另外,輸出文件會帶有.jsx擴展名。 react模式會生成React.createElement,在使用前不須要再進行轉換操做了,輸出文件的擴展名爲.js。 react-native至關於preserve,它也保留了全部的JSX,可是輸出文件的擴展名是.js。
模式
輸入
輸出
輸出文件擴展名
preserve
<div />
<div />
.jsx
react
<div />
React.createElement("div")
.js
react-native
<div />
<div />
.js
你能夠經過在命令行裏使用--jsx標記或tsconfig.json裏的選項來指定模式。
注意:React標識符是寫死的硬編碼,因此你必須保證React(大寫的R)是可用的。
as操做符
回想一下怎麼寫類型斷言:
var foo = <foo>bar;
這裏咱們斷言bar變量是foo類型的。 由於TypeScript也使用尖括號來表示類型斷言,JSX的語法帶來了解析的困難。所以,TypeScript在.tsx文件裏禁用了使用尖括號的類型斷言。
爲了彌補.tsx裏的這個功能,新加入了一個類型斷言符號:as。 上面的例子能夠很容易地使用as操做符改寫:
var foo = bar as foo;
as操做符在.ts和.tsx裏均可用,而且與其它類型斷言行爲是等價的。
類型檢查
爲了理解JSX的類型檢查,你必須首先理解固有元素與基於值的元素之間的區別。 假設有這樣一個JSX表達式<expr />,expr可能引用環境自帶的某些東西(好比,在DOM環境裏的div或span)或者是你自定義的組件。 這是很是重要的,緣由有以下兩點:
對於React,固有元素會生成字符串(React.createElement("div")),然而由你自定義的組件卻不會生成(React.createElement(MyComponent))。
傳入JSX元素裏的屬性類型的查找方式不一樣。 固有元素屬性自己就支持,然而自定義的組件會本身去指定它們具備哪一個屬性。
TypeScript使用與React相同的規範 來區別它們。 固有元素老是以一個小寫字母開頭,基於值的元素老是以一個大寫字母開頭。
固有元素
固有元素使用特殊的接口JSX.IntrinsicElements來查找。 默認地,若是這個接口沒有指定,會所有經過,不對固有元素進行類型檢查。 然而,若是這個接口存在,那麼固有元素的名字須要在JSX.IntrinsicElements接口的屬性裏查找。 例如:
declare namespace JSX {
interface IntrinsicElements {
foo: any
}
}
<foo />; // 正確
<bar />; // 錯誤
在上例中,<foo />沒有問題,可是<bar />會報錯,由於它沒在JSX.IntrinsicElements裏指定。
注意:你也能夠在JSX.IntrinsicElements上指定一個用來捕獲全部字符串索引:
declare namespace JSX {
interface IntrinsicElements {
}
}
基於值的元素
基於值的元素會簡單的在它所在的做用域裏按標識符查找。
import MyComponent from "./myComponent";
<MyComponent />; // 正確
<SomeOtherComponent />; // 錯誤
有兩種方式能夠定義基於值的元素:
無狀態函數組件 (SFC)
類組件
因爲這兩種基於值的元素在JSX表達式裏沒法區分,所以咱們首先會嘗試將表達式作爲無狀態函數組件進行解析。若是解析成功,那麼咱們就完成了表達式到其聲明的解析操做。若是按照無狀態函數組件解析失敗,那麼咱們會繼續嘗試以類組件的形式進行解析。若是依舊失敗,那麼將輸出一個錯誤。
無狀態函數組件
正如其名,組件被定義成JavaScript函數,它的第一個參數是props對象。 咱們強制它的返回值能夠賦值給JSX.Element。
interface FooProp {
name: string;
X: number;
Y: number;
}
declare function AnotherComponent(prop: {name: string});
function ComponentFoo(prop: FooProp) {
return <AnotherComponent name=prop.name />;
}
const Button = (prop: {value: string}, context: { color: string }) => <button>
因爲無狀態函數組件是簡單的JavaScript函數,因此咱們還能夠利用函數重載。
interface ClickableProps {
children: JSX.Element[] | JSX.Element
}
interface HomeProps extends ClickableProps {
home: JSX.Element;
}
interface SideProps extends ClickableProps {
side: JSX.Element | string;
}
function MainButton(prop: HomeProps): JSX.Element;
function MainButton(prop: SideProps): JSX.Element {
...
}
類組件
咱們能夠限制類組件的類型。 然而,爲了這麼作咱們須要引入兩個新的術語:元素類的類型和元素實例的類型。
如今有<Expr />,元素類的類型爲Expr的類型。 因此在上面的例子裏,若是MyComponent是ES6的類,那麼它的類類型就是這個類。 若是MyComponent是個工廠函數,類類型爲這個函數。
一旦創建起了類類型,實例類型就肯定了,爲類類型調用簽名的返回值與構造簽名的聯合類型。 再次說明,在ES6類的狀況下,實例類型爲這個類的實例的類型,而且若是是工廠函數,實例類型爲這個函數返回值類型。
class MyComponent {
render() {}
}
// 使用構造簽名
var myComponent = new MyComponent();
// 元素類的類型 => MyComponent
// 元素實例的類型 => { render: () => void }
function MyFactoryFunction() {
return {
render: () => {
}
}
}
// 使用調用簽名
var myComponent = MyFactoryFunction();
// 元素類的類型 => FactoryFunction
// 元素實例的類型 => { render: () => void }
元素的實例類型頗有趣,由於它必須賦值給JSX.ElementClass或拋出一個錯誤。 默認的JSX.ElementClass爲{},可是它能夠被擴展用來限制JSX的類型以符合相應的接口。
declare namespace JSX {
interface ElementClass {
render: any;
}
}
class MyComponent {
render() {}
}
function MyFactoryFunction() {
return { render: () => {} }
}
<MyComponent />; // 正確
<MyFactoryFunction />; // 正確
class NotAValidComponent {}
function NotAValidFactoryFunction() {
return {};
}
<NotAValidComponent />; // 錯誤
<NotAValidFactoryFunction />; // 錯誤
屬性類型檢查
屬性類型檢查的第一步是肯定元素屬性類型。 這在固有元素和基於值的元素之間稍有不一樣。
對於固有元素,這是JSX.IntrinsicElements屬性的類型。
declare namespace JSX {
interface IntrinsicElements {
foo: { bar?: boolean }
}
}
// foo
的元素屬性類型爲{bar?: boolean}
<foo bar />;
對於基於值的元素,就稍微複雜些。 它取決於先前肯定的在元素實例類型上的某個屬性的類型。 至於該使用哪一個屬性來肯定類型取決於JSX.ElementAttributesProperty。 它應該使用單一的屬性來定義。 這個屬性名以後會被使用。
declare namespace JSX {
interface ElementAttributesProperty {
props; // 指定用來使用的屬性名
}
}
class MyComponent {
// 在元素實例類型上指定屬性
props: {
foo?: string;
}
}
// MyComponent
的元素屬性類型爲{foo?: string}
<MyComponent foo="bar" />
元素屬性類型用於的JSX裏進行屬性的類型檢查。 支持可選屬性和必須屬性。
declare namespace JSX {
interface IntrinsicElements {
foo: { requiredProp: string; optionalProp?: number }
}
}
<foo requiredProp="bar" />; // 正確
<foo requiredProp="bar" optionalProp={0} />; // 正確
<foo />; // 錯誤, 缺乏 requiredProp
<foo requiredProp={0} />; // 錯誤, requiredProp 應該是字符串
<foo requiredProp="bar" unknownProp />; // 錯誤, unknownProp 不存在
<foo requiredProp="bar" some-unknown-prop />; // 正確, some-unknown-prop
不是個合法的標識符
注意:若是一個屬性名不是個合法的JS標識符(像data-*屬性),而且它沒出如今元素屬性類型裏時不會當作一個錯誤。
延展操做符也能夠使用:
var props = { requiredProp: 'bar' };
<foo {...props} />; // 正確
var badProps = {};
<foo {...badProps} />; // 錯誤
子孫類型檢查
從TypeScript 2.3開始,咱們引入了children類型檢查。children是元素屬性(attribute)類型的一個屬性(property)。 與使用JSX.ElementAttributesProperty來決定props名相似,咱們能夠利用JSX.ElementChildrenAttribute來決定children名。 JSX.ElementChildrenAttribute應該被聲明在單一的屬性(property)裏。
declare namespace JSX {
interface ElementChildrenAttribute {
children: {}; // specify children name to use
}
}
如不特殊指定子孫的類型,咱們將使用React typings裏的默認類型。
<div>
Hello
</div>;
<div>
Hello
World
</div>;
const CustomComp = (props) => <div>props.children</div>
<CustomComp>
<div>Hello World</div>
{"This is just a JS expression..." + 1000}
</CustomComp>
你也能夠像其它屬性同樣指定children的類型。下面咱們重寫React typings裏的默認類型。
interface PropsType {
children: JSX.Element
name: string
}
class Component extends React.Component<PropsType, {}> {
render() {
return (
<h2>
this.props.children
</h2>
)
}
}
// OK
<Component>
Hello World
</Component>
// Error: children is of type JSX.Element not array of JSX.Element
<Component>
Hello World
<h2>Hello World</h2>
</Component>
// Error: children is of type JSX.Element not array of JSX.Element or string.
<Component>
Hello
World
</Component>
JSX結果類型
默認地JSX表達式結果的類型爲any。 你能夠自定義這個類型,經過指定JSX.Element接口。 然而,不可以從接口裏檢索元素,屬性或JSX的子元素的類型信息。 它是一個黑盒。
嵌入的表達式
JSX容許你使用{ }標籤來內嵌表達式。
var a = <div>
{['foo', 'bar'].map(i => <span>{i / 2}</span>)}
</div>
上面的代碼產生一個錯誤,由於你不能用數字來除以一個字符串。 輸出以下,若你使用了preserve選項:
var a = <div>
{['foo', 'bar'].map(function (i) { return <span>{i / 2}</span>; })}
</div>
React整合
要想一塊兒使用JSX和React,你應該使用React類型定義。 這些類型聲明定義了JSX合適命名空間來使用React。
/// <reference path="react.d.ts" />
interface Props {
foo: string;
}
class MyComponent extends React.Component<Props, {}> {
render() {
return <span>{this.props.foo}</span>
}
}
<MyComponent foo="bar" />; // 正確
<MyComponent foo={0} />; // 錯誤
Decorators(裝飾器)
介紹
隨着TypeScript和ES6裏引入了類,在一些場景下咱們須要額外的特性來支持標註或修改類及其成員。 裝飾器(Decorators)爲咱們在類的聲明及成員上經過元編程語法添加標註提供了一種方式。 Javascript裏的裝飾器目前處在建議徵集的第二階段,但在TypeScript裏已作爲一項實驗性特性予以支持。
注意 裝飾器是一項實驗性特性,在將來的版本中可能會發生改變。
若要啓用實驗性的裝飾器特性,你必須在命令行或tsconfig.json裏啓用experimentalDecorators編譯器選項:
命令行:
tsc --target ES5 --experimentalDecorators
tsconfig.json:
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true
}
}
裝飾器
裝飾器是一種特殊類型的聲明,它可以被附加到類聲明,方法,訪問符,屬性或參數上。 裝飾器使用@expression這種形式,expression求值後必須爲一個函數,它會在運行時被調用,被裝飾的聲明信息作爲參數傳入。br/>例如,有一個@sealed裝飾器,咱們會這樣定義sealed函數:
function sealed(target) {
// do something with "target" ...
}
注意 後面類裝飾器小節裏有一個更加詳細的例子。
裝飾器工廠
若是咱們要定製一個修飾器如何應用到一個聲明上,咱們得寫一個裝飾器工廠函數。 裝飾器工廠就是一個簡單的函數,它返回一個表達式,以供裝飾器在運行時調用。
咱們能夠經過下面的方式來寫一個裝飾器工廠函數:
function color(value: string) { // 這是一個裝飾器工廠
return function (target) { // 這是裝飾器
// do something with "target" and "value"...
}
}
注意 下面方法裝飾器小節裏有一個更加詳細的例子。
裝飾器組合
多個裝飾器能夠同時應用到一個聲明上,就像下面的示例:
書寫在同一行上:
@f @g xbr/>書寫在多行上:
@f
@g
x
當多個裝飾器應用於一個聲明上,它們求值方式與複合函數類似。在這個模型下,當複合f和g時,複合的結果(f ∘ g)(x)等同於f(g(x))。
一樣的,在TypeScript裏,當多個裝飾器應用在一個聲明上時會進行以下步驟的操做:
由上至下依次對裝飾器表達式求值。
求值的結果會被看成函數,由下至上依次調用。
若是咱們使用裝飾器工廠的話,能夠經過下面的例子來觀察它們求值的順序:
function f() {
console.log("f(): evaluated");
return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("f(): called");
}
}
function g() {
console.log("g(): evaluated");
return function (target, propertyKey: string, descriptor: PropertyDescriptor) {
console.log("g(): called");
}
}
class C {br/>@f()
@g()
method() {}
}
在控制檯裏會打印出以下結果:
f(): evaluated
g(): evaluated
g(): called
f(): called
裝飾器求值
類中不一樣聲明上的裝飾器將按如下規定的順序應用:
參數裝飾器,而後依次是方法裝飾器,訪問符裝飾器,或屬性裝飾器應用到每一個實例成員。
參數裝飾器,而後依次是方法裝飾器,訪問符裝飾器,或屬性裝飾器應用到每一個靜態成員。
參數裝飾器應用到構造函數。
類裝飾器應用到類。
類裝飾器
類裝飾器在類聲明以前被聲明(緊靠着類聲明)。 類裝飾器應用於類構造函數,能夠用來監視,修改或替換類定義。 類裝飾器不能用在聲明文件中(.d.ts),也不能用在任何外部上下文中(好比declare的類)。
類裝飾器表達式會在運行時看成函數被調用,類的構造函數做爲其惟一的參數。
若是類裝飾器返回一個值,它會使用提供的構造函數來替換類的聲明。
注意 若是你要返回一個新的構造函數,你必須注意處理好原來的原型鏈。 在運行時的裝飾器調用邏輯中不會爲你作這些。br/>下面是使用類裝飾器(@sealed)的例子,應用在Greeter類:
@sealed
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;br/>}
}
咱們能夠這樣定義@sealed裝飾器:
function sealed(constructor: Function) {br/>Object.seal(constructor);
Object.seal(constructor.prototype);
}
當@sealed被執行的時候,它將密封此類的構造函數和原型。(注:參見Object.seal)
下面是一個重載構造函數的例子。
function classDecorator<T extends {new(...args:any[]):{}}>(constructor:T) {
return class extends constructor {
newProperty = "new property";
hello = "override";
}
}
@classDecorator
class Greeter {
property = "property";
hello: string;
constructor(m: string) {
this.hello = m;
}
}
console.log(new Greeter("world"));
方法裝飾器
方法裝飾器聲明在一個方法的聲明以前(緊靠着方法聲明)。 它會被應用到方法的屬性描述符上,能夠用來監視,修改或者替換方法定義。 方法裝飾器不能用在聲明文件(.d.ts),重載或者任何外部上下文(好比declare的類)中。
方法裝飾器表達式會在運行時看成函數被調用,傳入下列3個參數:
對於靜態成員來講是類的構造函數,對於實例成員是類的原型對象。
成員的名字。
成員的屬性描述符。
注意 若是代碼輸出目標版本小於ES5,屬性描述符將會是undefined。
若是方法裝飾器返回一個值,它會被用做方法的屬性描述符。
注意 若是代碼輸出目標版本小於ES5返回值會被忽略。br/>下面是一個方法裝飾器(@enumerable)的例子,應用於Greeter類的方法上:
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
@enumerable(false) greet() { return "Hello, " + this.greeting; }
}br/>咱們能夠用下面的函數聲明來定義@enumerable裝飾器:
function enumerable(value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.enumerable = value;
};
}
這裏的@enumerable(false)是一個裝飾器工廠。 當裝飾器@enumerable(false)被調用時,它會修改屬性描述符的enumerable屬性。
訪問器裝飾器
訪問器裝飾器聲明在一個訪問器的聲明以前(緊靠着訪問器聲明)。 訪問器裝飾器應用於訪問器的屬性描述符而且能夠用來監視,修改或替換一個訪問器的定義。 訪問器裝飾器不能用在聲明文件中(.d.ts),或者任何外部上下文(好比declare的類)裏。
注意 TypeScript不容許同時裝飾一個成員的get和set訪問器。取而代之的是,一個成員的全部裝飾的必須應用在文檔順序的第一個訪問器上。這是由於,在裝飾器應用於一個屬性描述符時,它聯合了get和set訪問器,而不是分開聲明的。
訪問器裝飾器表達式會在運行時看成函數被調用,傳入下列3個參數:
對於靜態成員來講是類的構造函數,對於實例成員是類的原型對象。
成員的名字。
成員的屬性描述符。
注意 若是代碼輸出目標版本小於ES5,Property Descriptor將會是undefined。
若是訪問器裝飾器返回一個值,它會被用做方法的屬性描述符。
注意 若是代碼輸出目標版本小於ES5返回值會被忽略。br/>下面是使用了訪問器裝飾器(@configurable)的例子,應用於Point類的成員上:
class Point {
private _x: number;
private _y: number;
constructor(x: number, y: number) {
this._x = x;
this._y = y;
}
@configurable(false) get x() { return this._x; } @configurable(false) get y() { return this._y; }
}br/>咱們能夠經過以下函數聲明來定義@configurable裝飾器:
function configurable(value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.configurable = value;
};
}
屬性裝飾器
屬性裝飾器聲明在一個屬性聲明以前(緊靠着屬性聲明)。 屬性裝飾器不能用在聲明文件中(.d.ts),或者任何外部上下文(好比declare的類)裏。
屬性裝飾器表達式會在運行時看成函數被調用,傳入下列2個參數:
對於靜態成員來講是類的構造函數,對於實例成員是類的原型對象。
成員的名字。
注意 屬性描述符不會作爲參數傳入屬性裝飾器,這與TypeScript是如何初始化屬性裝飾器的有關。 由於目前沒有辦法在定義一個原型對象的成員時描述一個實例屬性,而且沒辦法監視或修改一個屬性的初始化方法。返回值也會被忽略。 所以,屬性描述符只能用來監視類中是否聲明瞭某個名字的屬性。
若是訪問符裝飾器返回一個值,它會被用做方法的屬性描述符。
咱們能夠用它來記錄這個屬性的元數據,以下例所示:
class Greeter {
@format("Hello, %s")
greeting: string;
constructor(message: string) { this.greeting = message; } greet() { let formatString = getFormat(this, "greeting"); return formatString.replace("%s", this.greeting); }
}br/>而後定義@format裝飾器和getFormat函數:
import "reflect-metadata";
const formatMetadataKey = Symbol("format");
function format(formatString: string) {
return Reflect.metadata(formatMetadataKey, formatString);
}
function getFormat(target: any, propertyKey: string) {
return Reflect.getMetadata(formatMetadataKey, target, propertyKey);
}
這個@format("Hello, %s")裝飾器是個 裝飾器工廠。 當@format("Hello, %s")被調用時,它添加一條這個屬性的元數據,經過reflect-metadata庫裏的Reflect.metadata函數。 當getFormat被調用時,它讀取格式的元數據。
注意 這個例子須要使用reflect-metadata庫。 查看元數據瞭解reflect-metadata庫更詳細的信息。
參數裝飾器
參數裝飾器聲明在一個參數聲明以前(緊靠着參數聲明)。 參數裝飾器應用於類構造函數或方法聲明。 參數裝飾器不能用在聲明文件(.d.ts),重載或其它外部上下文(好比declare的類)裏。
參數裝飾器表達式會在運行時看成函數被調用,傳入下列3個參數:
對於靜態成員來講是類的構造函數,對於實例成員是類的原型對象。
成員的名字。
參數在函數參數列表中的索引。
注意 參數裝飾器只能用來監視一個方法的參數是否被傳入。br/>參數裝飾器的返回值會被忽略。
下例定義了參數裝飾器(@required)並應用於Greeter類方法的一個參數:
class Greeter {
greeting: string;
constructor(message: string) { this.greeting = message; } @validate greet(@required name: string) { return "Hello " + name + ", " + this.greeting; }
}
而後咱們使用下面的函數定義 @required 和 @validate 裝飾器:
import "reflect-metadata";
const requiredMetadataKey = Symbol("required");
function required(target: Object, propertyKey: string | symbol, parameterIndex: number) {
let existingRequiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || [];
existingRequiredParameters.push(parameterIndex);
Reflect.defineMetadata(requiredMetadataKey, existingRequiredParameters, target, propertyKey);
}
function validate(target: any, propertyName: string, descriptor: TypedPropertyDescriptor<Function>) {
let method = descriptor.value;
descriptor.value = function () {
let requiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyName);
if (requiredParameters) {
for (let parameterIndex of requiredParameters) {
if (parameterIndex >= arguments.length || arguments[parameterIndex] === undefined) {
throw new Error("Missing required argument.");
}
}
}
return method.apply(this, arguments); }
}
@required裝飾器添加了元數據實體把參數標記爲必需的。 @validate裝飾器把greet方法包裹在一個函數裏在調用原先的函數前驗證函數參數。
注意 這個例子使用了reflect-metadata庫。 查看元數據瞭解reflect-metadata庫的更多信息。
元數據
一些例子使用了reflect-metadata庫來支持實驗性的metadata API。 這個庫還不是ECMAScript (JavaScript)標準的一部分。 然而,當裝飾器被ECMAScript官方標準採納後,這些擴展也將被推薦給ECMAScript以採納。
你能夠經過npm安裝這個庫:
npm i reflect-metadata --save
TypeScript支持爲帶有裝飾器的聲明生成元數據。 你須要在命令行或tsconfig.json裏啓用emitDecoratorMetadata編譯器選項。
Command Line:
tsc --target ES5 --experimentalDecorators --emitDecoratorMetadata
tsconfig.json:
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
當啓用後,只要reflect-metadata庫被引入了,設計階段添加的類型信息能夠在運行時使用。
以下例所示:
import "reflect-metadata";
class Point {
x: number;
y: number;
}
class Line {
private _p0: Point;
private _p1: Point;
@validate set p0(value: Point) { this._p0 = value; } get p0() { return this._p0; } @validate set p1(value: Point) { this._p1 = value; } get p1() { return this._p1; }
}
function validate<T>(target: any, propertyKey: string, descriptor: TypedPropertyDescriptor<T>) {
let set = descriptor.set;
descriptor.set = function (value: T) {
let type = Reflect.getMetadata("design:type", target, propertyKey);
if (!(value instanceof type)) {
throw new TypeError("Invalid type.");
}
set(value);
}
}
TypeScript編譯器能夠經過@Reflect.metadata裝飾器注入設計階段的類型信息。 你能夠認爲它至關於下面的TypeScript:
class Line {
private _p0: Point;
private _p1: Point;
@validate @Reflect.metadata("design:type", Point) set p0(value: Point) { this._p0 = value; } get p0() { return this._p0; } @validate @Reflect.metadata("design:type", Point) set p1(value: Point) { this._p1 = value; } get p1() { return this._p1; }
}
混入(mixin)
class Disposable {
isDisposed: boolean;
dispose() {
this.isDisposed = true;
}
}
// Activatable Mixin
class Activatable {
isActive: boolean;
activate() {
this.isActive = true;
}
deactivate() {
this.isActive = false;
}
}
class SmartObject implements Disposable, Activatable {
constructor() {
setInterval(() => console.log(this.isActive + " : " + this.isDisposed), 500);
}
interact() { this.activate(); } // Disposable isDisposed: boolean = false; dispose: () => void; // Activatable isActive: boolean = false; activate: () => void; deactivate: () => void;
}
applyMixins(SmartObject, [Disposable, Activatable]);
let smartObj = new SmartObject();
setTimeout(() => smartObj.interact(), 1000);
////////////////////////////////////////
// In your runtime library somewhere
////////////////////////////////////////
function applyMixins(derivedCtor: any, baseCtors: any[]) {
baseCtors.forEach(baseCtor => {
Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
derivedCtor.prototype[name] = baseCtor.prototype[name];
});
});
}
理解這個例子
代碼裏首先定義了兩個類,它們將作爲mixins。 能夠看到每一個類都只定義了一個特定的行爲或功能。 稍後咱們使用它們來建立一個新類,同時具備這兩種功能。
// Disposable Mixin
class Disposable {
isDisposed: boolean;
dispose() {
this.isDisposed = true;
}
}
// Activatable Mixin
class Activatable {
isActive: boolean;
activate() {
this.isActive = true;
}
deactivate() {
this.isActive = false;
}
}
下面建立一個類,結合了這兩個mixins。 下面來看一下具體是怎麼操做的:
class SmartObject implements Disposable, Activatable {
首先應該注意到的是,沒使用extends而是使用implements。 把類當成了接口,僅使用Disposable和Activatable的類型而非其實現。 這意味着咱們須要在類裏面實現接口。 可是這是咱們在用mixin時想避免的。
咱們能夠這麼作來達到目的,爲將要mixin進來的屬性方法建立出佔位屬性。 這告訴編譯器這些成員在運行時是可用的。 這樣就能使用mixin帶來的便利,雖然說須要提早定義一些佔位屬性。
// Disposable
isDisposed: boolean = false;
dispose: () => void;
// Activatable
isActive: boolean = false;
activate: () => void;
deactivate: () => void;
最後,把mixins混入定義的類,完成所有實現部分。
applyMixins(SmartObject, [Disposable, Activatable]);
最後,建立這個幫助函數,幫咱們作混入操做。 它會遍歷mixins上的全部屬性,並複製到目標上去,把以前的佔位屬性替換成真正的實現代碼。
function applyMixins(derivedCtor: any, baseCtors: any[]) {
baseCtors.forEach(baseCtor => {
Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
derivedCtor.prototype[name] = baseCtor.prototype[name];
})
});
}
三斜線指令
三斜線指令是包含單個XML標籤的單行註釋。 註釋的內容會作爲編譯器指令使用。
三斜線指令僅可放在包含它的文件的最頂端。 一個三斜線指令的前面只能出現單行或多行註釋,這包括其它的三斜線指令。 若是它們出如今一個語句或聲明以後,那麼它們會被當作普通的單行註釋,而且不具備特殊的涵義。
/// <reference path="..." />
/// <reference path="..." />指令是三斜線指令中最多見的一種。 它用於聲明文件間的依賴。
三斜線引用告訴編譯器在編譯過程當中要引入的額外的文件。
當使用--out或--outFile時,它也能夠作爲調整輸出內容順序的一種方法。 文件在輸出文件內容中的位置與通過預處理後的輸入順序一致。
預處理輸入文件
編譯器會對輸入文件進行預處理來解析全部三斜線引用指令。 在這個過程當中,額外的文件會加到編譯過程當中。
這個過程會以一些根文件開始; 它們是在命令行中指定的文件或是在tsconfig.json中的"files"列表裏的文件。 這些根文件按指定的順序進行預處理。 在一個文件被加入列表前,它包含的全部三斜線引用都要被處理,還有它們包含的目標。 三斜線引用以它們在文件裏出現的順序,使用深度優先的方式解析。
一個三斜線引用路徑是相對於包含它的文件的,若是不是根文件。
錯誤
引用不存在的文件會報錯。 一個文件用三斜線指令引用本身會報錯。
使用 --noResolve
若是指定了--noResolve編譯選項,三斜線引用會被忽略;它們不會增長新文件,也不會改變給定文件的順序。
/// <reference types="..." />
與/// <reference path="..." />指令類似,這個指令是用來聲明依賴的; 一個/// <reference path="..." />指令聲明瞭對@types包的一個依賴。
在聲明文件裏包含/// <reference types="node" />,代表這個文件使用了@types/node/index.d.ts裏面聲明的名字; 而且,這個包要在編譯階段與聲明文件一塊兒被包含進來。
解析@types包的名字的過程與解析import語句裏模塊名的過程相似。 因此能夠簡單的把三斜線類型引用指令想像成針對包的import聲明。
僅當在你須要寫一個d.ts文件時才使用這個指令。
對於那些在編譯階段生成的聲明文件,編譯器會自動地添加/// <reference types="..." />; 當且僅當結果文件中使用了引用的@types包裏的聲明時纔會在生成的聲明文件裏添加/// <reference types="..." />語句。
若要在.ts文件裏聲明一個對@types包的依賴,使用--types命令行選項或在tsconfig.json裏指定。 查看在tsconfig.json裏使用@types,typeRoots和types瞭解詳情。
/// <reference no-default-lib="true"/>
這個指令把一個文件標記成默認庫。 你會在lib.d.ts文件和它不一樣的變體的頂端看到這個註釋。
這個指令告訴編譯器在編譯過程當中不要包含這個默認庫(好比,lib.d.ts)。 這與在命令行上使用--noLib類似。
還要注意,當傳遞了--skipDefaultLibCheck時,編譯器只會忽略檢查帶有/// <reference no-default-lib="true"/>的文件。
/// <amd-module />
默認狀況下生成的AMD模塊都是匿名的。 可是,當一些工具須要處理生成的模塊時會產生問題,好比r.js。
amd-module指令容許給編譯器傳入一個可選的模塊名:
amdModule.ts
///<amd-module name='NamedModule'/>
export class C {
}
這會將NamedModule傳入到AMD define函數裏:
amdModule.js
define("NamedModule", ["require", "exports"], function (require, exports) {
var C = (function () {
function C() {
}
return C;
})();
exports.C = C;
});
/// <amd-dependency />
注意:這個指令被廢棄了。使用import "moduleName";語句代替。
/// <amd-dependency path="x" />告訴編譯器有一個非TypeScript模塊依賴須要被注入,作爲目標模塊require調用的一部分。
amd-dependency指令也能夠帶一個可選的name屬性;它容許咱們爲amd-dependency傳入一個可選名字:
/// <amd-dependency path="legacy/moduleA" name="moduleA"/>
declare var moduleA:MyType
moduleA.callStuff()
生成的JavaScript代碼:
define(["require", "exports", "legacy/moduleA"], function (require, exports, moduleA) {
moduleA.callStuff()
});
JavaScript文件裏的類型檢查
TypeScript 2.3之後的版本支持使用--checkJs對.js文件進行類型檢查並提示錯誤的模式。
你能夠經過添加// @ts-nocheck註釋來忽略類型檢查;相反你能夠經過去掉--checkJs設置並添加// @ts-check註釋來選則檢查某些.js文件。 你還能夠使用// @ts-ignore來忽略本行的錯誤。
下面是一些值得注意的類型檢查在.js文件與.ts文件上的差別:
在JSDoc上使用類型
.js文件裏,類型能夠和在.ts文件裏同樣被推斷出來。 一樣地,當類型不能被推斷時,它們能夠經過JSDoc來指定,就比如在.ts文件裏那樣。
JSDoc註解修飾的聲明會被設置爲這個聲明的類型。好比:
/* @type {number} /
var x;
x = 0; // OK
x = false; // Error: boolean is not assignable to number
你能夠在這裏找到全部JSDoc支持的模式,JSDoc文檔。
從類內部賦值語句推斷屬性聲明
ES2015/ES6不存在類屬性的聲明。屬性是動態的賦予的,就如同對象字面量同樣。
在.js文件裏,屬性聲明是由類內部的屬性賦值語句推斷出來的。屬性的類型是賦值語句右側全部值的聯合。構造函數裏定義的屬性是永遠存在的,在方法存取器裏定義的被認爲是可選的。
使用JSDoc修飾屬性賦值來指定屬性類型。例如:
class C {
constructor() {
/* @type {number | undefined} /
this.prop = undefined;
}
}
let c = new C();
c.prop = 0; // OK
c.prop = "string"; // Error: string is not assignable to number|undefined
若是屬性永遠都不在類的內部被設置,那麼它們被當成是未知的。若是類具備只讀的屬性,考慮在構造函數裏給它初始化成undefined,例如this.prop = undefined;。
CommonJS模塊輸入支持
.js文件支持將CommonJS模塊作爲輸入模塊格式。對exports和module.exports的賦值被識別爲導出聲明。 類似地,require函數調用被識別爲模塊導入。例如:
// import module "fs"
const fs = require("fs");
// export function readFile
module.exports.readFile = function(f) {
return fs.readFileSync(f);
}
對象字面量是開放的
默認地,變量聲明中的對象字面量自己就提供了類型聲明。新的成員不能被加到對象中去。 這個規則在.js文件裏被放寬了;對象字面量具備開放的類型,容許添加並訪問原先沒有定義的屬性。例如:
var obj = { a: 1 };
obj.b = 2; // Allowed
對象字面量具備默認的索引簽名[x:string]: any,它們能夠被當成開放的映射而不是封閉的對象。
與其它JS檢查行爲類似,這種行爲能夠經過指定JSDoc類型來改變,例如:
/* @type /
var obj = { a: 1 };
obj.b = 2; // Error, type {a: number} does not have property b
函數參數是默承認選的
因爲JS不支持指定可選參數(不指定一個默認值),.js文件裏全部函數參數都被當作可選的。使用比預期少的參數調用函數是容許的。
須要注意的一點是,使用過多的參數調用函數會獲得一個錯誤。
例如:
function bar(a, b){
console.log(a + " " + b);
}
bar(1); // OK, second argument considered optional
bar(1, 2);
bar(1, 2, 3); // Error, too many arguments
使用JSDoc註解的函數會被從這條規則裏移除。使用JSDoc可選參數語法來表示可選性。好比:
/**
sayHello();
由arguments推斷出的var-args參數聲明
若是一個函數的函數體內有對arguments的引用,那麼這個函數會隱式地被認爲具備一個var-arg參數(好比:(...arg: any[]) => any))。使用JSDoc的var-arg語法來指定arguments的類型。
未指定的類型參數默認爲any
未指定的泛型參數類型將默認爲any。有以下幾種情形:
在extends語句中
例如,React.Component被定義成具備兩個泛型參數,Props和State。 在一個.js文件裏,沒有一個合法的方式在extends語句裏指定它們。默認地參數類型爲any:
import { Component } from "react";
class MyComponent extends Component {
render() {
this.props.b; // Allowed, since this.props is of type anybr/>}
}
使用JSDoc的@augments來明確地指定類型。例如:
import { Component } from "react";
/**
x.push(1); // OK
x.push("string"); // OK, x is of type Array<any>
/* @type{Array.<number>} /
var y = [];
y.push(1); // OK
y.push("string"); // Error, string is not assignable to number
在函數調用中
泛型函數的調用使用arguments來推斷泛型參數。有時候,這個流程不可以推斷出類型,大可能是由於缺乏推斷的源;在這種狀況下,泛型參數類型默認爲any。例如:
var p = new Promise((resolve, reject) => { reject() });
p; // Promise<any>;