typescript 簡明教程

TypeScript 是 JavaScript 的類型的超集,其增長了代碼的可讀性和可維護性,它會進行靜態檢查,若是發現有錯誤,編譯的時候就會報錯(雖然會報錯了,但仍是會生成編譯結果)。javascript

本文大量參考官方文檔,如需深刻學習請前往官方查看,本文目的只是一些內容的精簡記錄,方便查詢(本文是在官方文檔基礎上加上谷歌翻譯、zhongsp的typescript使用手冊TypeScript 入門教程黯羽輕揚的typescript筆記和本身的理解,因此文字方面有些是我本身的語言,不過代碼方面均基於3.9的環境運行過)java

基於版本3.9,會不按期更新node

Demo

  • 安裝
npm install -g typescript // typescript
npm install -g ts-node // 在node環境下直接編譯運行ts文件
...
/// 經過 tsc -V 查看安裝是否成功
複製代碼
  • 初始化
tsc --init // 任意目錄初始化tsconfig
複製代碼
  • 修改配置文件中最基本的配置信息
/// tsconfig.json
{
	"compilerOptions": {
		...
		// "outDir": "./", // 把此處的輸出路徑打開
	},
	"files": ["index.ts"] // 指定編譯文件
}
複製代碼
  • 運行
/// 編譯,因爲運行的是默認配置,能夠簡寫爲 tsc
tsc -p ./tsconfig.json
...
/// 在node環境直接運行
ts-node index.ts
複製代碼

基礎類型

typescript 兼容 javascript 的數據類型,javascript 有下面七種數據類型:react

  • 布爾
  • 數字
  • 字符串
  • undefined
  • null
  • object
  • symbol

布爾值

let isDone: boolean = false;
複製代碼

布爾值指的是 boolean,而非 Boolean 這個包裝對象jquery

let createdByNewBoolean: boolean = new Boolean(1);
// 不能將類型「Boolean」分配給類型「boolean」。
  「boolean」是基元,但「Boolean」是包裝器對象。如可能首選使用「boolean」。ts(2322)
...
let createdByNewBoolean: Boolean = new Boolean(1); // 這是能夠的
let createdByBoolean: boolean = Boolean(1); // 這也能夠
複製代碼

數字

和JavaScript同樣,TypeScript裏的全部數字都是浮點數。 這些浮點數的類型是 numbergit

let decLiteral: number = 6; // 10進制
let hexLiteral: number = 0xf00d; // 16進制
let binaryLiteral: number = 0b1010; // 2進制
let octalLiteral: number = 0o744; // 8進制
複製代碼

字符串

使用 string表示文本數據類型。 和 JavaScript 同樣,可使用雙引號( ")或單引號(')表示字符串es6

let name: string = "bob";
複製代碼

支持模版字符串github

let name: string = 'Gene';
let sentence: string = `Hello, my name is ${ name }.`
複製代碼

數組

  • 第一種,能夠在元素類型後面接上 [],表示由此類型元素組成的一個數組
let list: number[] = [1, 2, 3];
複製代碼
  • 第二種方式是使用數組泛型,Array<元素類型>
let list: Array<number> = [1, 2, 3];
複製代碼

元祖 Tuple

元組類型容許表示一個已知元素數量和類型的數組,各元素的類型沒必要相同ajax

// Declare a tuple type
let x: [string, number];
// Initialize it
x = ['hello', 10]; // OK
...
x = [10, 'hello']; // Error
複製代碼

當訪問一個已知索引的元素,會獲得正確的類型typescript

console.log(x[0].substr(1)); // OK
console.log(x[1].substr(1)); // Error, 'number' does not have 'substr'
複製代碼

訪問已知索引外的元素會失敗,並顯示如下錯誤

x[3] = 'world'; // Error Property '3' does not exist on type '[string, number] 複製代碼

枚舉

enum 枚舉是一種爲數字值集,賦予更友好名稱的方法

enum Color {Red, Green, Blue}
let c: Color = Color.Green;
複製代碼

默認狀況下,從0開始爲元素編號。 你也能夠手動的指定成員的數值,或者所有都採用手動賦值

enum Color {Red = 1, Green = 2, Blue = 4}
let c: Color = Color.Green; // c爲2
複製代碼

這有一個比較有用的場景

enum fetchStatus {
    success = '000000',
    noLogin = '910001',
    timeOut = '910002'
}
if (data.code === fetchStatus.success) {...}
/// 這裏能夠假定登錄接口返回的 code 碼有多種含義,經過枚舉方式的定義,能增長代碼的可讀性
複製代碼

Any

爲那些在編程階段還不清楚類型的變量指定一個類型。 這些值可能來自於動態的內容,好比來自用戶輸入或第三方代碼庫。

let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false; // okay, definitely a boolean
複製代碼

若是隻知道部分數據類型時,也可使用any類型

let arr: Array<any> = [2, 'ss']
複製代碼

any 類型上訪問任何屬性都是容許的

let anyThing: any = 'hello';
console.log(anyThing.myName);
console.log(anyThing.myName.firstName);
複製代碼

也容許調用任何方法

let anyThing: any = 'Tom';
anyThing.setName('Jerry');
anyThing.setName('Jerry').sayHello();
anyThing.myName.setFirstName('Cat');
複製代碼

聲明一個變量爲任意值以後,對它的任何操做,返回的內容的類型都是任意值(此處能夠類比成一個js變量,想作什麼都想,在編譯階段都不會報錯,只有在運行時才知對應信息存不存在)

Void

void 類型像是與 any 類型相反,它表示沒有任何類型。當一個函數沒有返回值時,返回值類型能夠是 void

function warnUser(): void {
    console.log("This is my warning message");
}
複製代碼

聲明爲void類型的變量只能爲其賦值 undefinednullnull 須要修改 tsconfig.json 中的 strictNullChecks 打開配置)

let unusable: void = undefined;
複製代碼

Null 和 Undefined

undefinednull 二者各自有本身的類型分別叫作 undefinednull ,默認狀況下 nullundefined 是全部類型的子類型

let u: undefined = undefined;
let n: null = null;
複製代碼

當指定了--strictNullChecks標記,null和undefined只能賦值給void和它們各自

Never

never 類型表示的是那些永不存在的值的類型,好比那些老是會拋出異常或根本就不會有返回值的函數表達式或箭頭函數表達式的返回值類型

function error(message: string): never {
	throw new Error(message);
}
複製代碼

另一個用處就是在處理類型收窄, 避免出現新增了聯合類型沒有對應的實現

/// https://www.zhihu.com/question/354601204 參考尤雨溪的回答 
interface Foo {
	type: 'foo'
}

interface Bar {
	type: 'bar'
}

type All = Foo | Bar

function handleValue(val: All) {
  switch (val.type) {
    case 'foo':
    // 這裏 val 被收窄爲 Foo
    break
  case 'bar':
    // val 在這裏是 Bar
    break
  default:
    // val 在這裏是 never
    const exhaustiveCheck: never = val
    break
  }
}
/// 如上All 是一個聯合類型,handleValue 中已經把 All 在定義時,全部的可能值都作了處理,最後在 default 中設置爲 never,若是另一我的改了 All 的類型,那麼就必須在 handleValue 中進行對應處理,從而達到保護的做用
複製代碼

Object

object表示非原始類型,也就是除number,string,boolean,symbol,null或undefined以外的類型。

類型斷言

類型斷言能夠用來手動指定一個值的類型,在後續的聯合類型的介紹中會再介紹

  • 類型斷言有兩種形式。 其一是「尖括號」語法
  • 另外一個是 as 語法,第二種在JSX中能夠正常使用
/// 下面是官網的例子,不過考慮以前 any 的特性,這裏其實不用斷言,也同樣能正常使用
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
...
/// 有時會不免會爲 window 添加某些屬性,直接這麼寫 ts 會報錯,告訴不存在這個屬性
window.foo = 1;
...
(<any>window).foo = 1; // <>寫法
(window as any).foo = 1; // as寫法
複製代碼

類型推論

若是沒有明確的指定類型,那麼 TypeScript 會依照類型推論(Type Inference)的規則推斷出一個類型

經過值推導類型

let info = 'info';
info = 7; // Error 此處會推導 info 是 string 類型,因此不能把 number 賦值給 info
...
// 若是定義的時候沒有賦值,無論以後有沒有賦值,都會被推斷成 any 類型而徹底不被類型檢查
let info;
info = 'info'; 
info = 7; // 兩次賦值正確
複製代碼

當須要從幾個表達式中,推斷類型時。會使用這些表達式的類型,推斷出一個最合適的通用類型

let zoo = [new Rhino(), new Elephant(), new Snake()];
// 此時 ``zoo`` 會被推到爲聯合數組類型 ``(Rhino | Elephant | Snake)[]``
...
let zoo: Animal[] = [new Rhino(), new Elephant(), new Snake()];
// 此處 ``zoo`` 有明確的指示, 因此爲 ``Animal[]`` 類型
複製代碼

經過上下文推導表達式類型

參考知乎文章

  • 變量聲明式語句包含在類型聲明,參數聲明,屬性聲明,屬性解構等 變量聲明式語句 (VariableLikeDeclaration) 中時,能夠從聲明中進行上下文相關類型推導
const s: (v: string) => void = v => {};
/// s 是一個函數類型,v 並無明確的指明類型,但此處 ts 會推導其爲 string 類型
...
function f (a: (v: string) => void = vv => {}) {}
/// 函數的參數類型是 (v: string) => void,默認值是 vv => {},因此推導出 vv 是 string 類型
...
const s1: (a: {foo: (v: string) => void}) => void = ({foo = vv => {}}) => {}
/// s1 的類型是 (a: {foo: (v: string) => void}) => void ,對應賦值爲 ({foo = vv => {}}) => {}
/// 第二層:賦值的函數 a 的類型是 {foo: (v: string) => void} 對應賦值爲 {foo = vv => {}}
/// 第三層,這裏的 foo 對應的類型 (v: string) => void 就是 vv => {},因此,推導出 vv 仍是 string
...
class C1 {
    foo: (v: string) => void = vv => {}
}
/// 這裏 foo 的類型是 (v: string) => void 對應賦值爲 vv => {},vv 仍是 string
複製代碼

總的來講這種推導規則稍微的有些繞,分析使用狀況,能推導出對應值的屬性,這個更可能是實現細節,通常使用時,要麼寫明規則,要麼徹底不寫重回 js 模式

變量聲明

解構

數組解構

let input = [1, 2];
let [first, second] = input;
複製代碼

能夠在數組裏使用 ... 語法建立剩餘變量

let [first, ...rest] = [1, 2, 3, 4];
複製代碼

元組解構

元祖是一種特殊數組,因此數組的解析規則使用元組

let tuple: [number, string, boolean] = [7, "hello", true];
let [a, b, c] = tuple; // a: number, b: string, c: boolean
複製代碼

對象解構

let o = {
    a: "foo",
    b: 12,
    c: "bar"
};
let { a, b } = o;
複製代碼
  • 屬性重命名
let { a: newName1, b: newName2 } = o;
複製代碼

若是要指定類型能夠這麼寫

let {a, b}: {a: string, b: number} = o;
複製代碼
  • 對象解構時設置默認值
function add(input: {a:number, b:number, c?:number}) {
    let {a, b, c = 100} = input;
    return a + b + c;
}
複製代碼

ts 會比 js 多了些類型檢查,容易形成在設置解構時看起來很長,不容易理解

/// 分清是給參數賦默認值,仍是給解構賦默認值
/// 下面是給參數賦默認值
function add(input: {a:number, b:number, c?:number} = {a:0, b:0, c:100}) {
    let {a, b, c = 100} = input;
    return a + b + c;
}
add({a: 3, b: 2}); // [3, 0]
add();
...
/// 下面是給解構賦予默認值
function add1({a = 0, b = 0, c = 100}: {a?:number, b?:number, c?:number}) {
    return a + b + c;
}
add1({a: 3, b: 2}); // [3, 0]
add1({}); // [0, 0]
複製代碼

展開

展開進行的是淺拷貝

數組的規則簡單,直接展開變成一個新的數組

let first = [1, 2];
let second = [3, 4];
let bothPlus = [0, ...first, ...second, 5];
複製代碼

對象展開時,因爲從左往右進行,因此後面的屬性值會覆蓋前面的同名屬性

let defaults = { food: "spicy", price: "$$", ambiance: "noisy" };
let search = { ...defaults, food: "rich" };
複製代碼

索引類型

TypeScript 的核心原則之一是對值所具備的結構進行類型檢查。 它有時被稱作**「鴨式辨型法」「結構性子類型化」**。 在 TypeScript 裏,接口的做用就是爲這些類型命名和爲你的代碼或第三方代碼定義契約

interface LabelledValue {
  label: string;
}

function printLabel(labelledObj: LabelledValue) {
  console.log(labelledObj.label);
}

let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);
複製代碼

interface LabelledValue 就是定義的接口,表明了有一個 label 屬性且類型爲 string 的對象

可選屬性

interface SquareConfig {
  color?: string;
  width?: number;
}
複製代碼

接口下的屬性值,可存在也可不存在,除可選屬性外沒法賦值

只讀屬性

  • readonly 指定的屬性僅在首次建立對象時才能夠修改
interface Point {
    readonly x: number;
    readonly y: number;
}
...
let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error!
複製代碼
  • TS 具備 ReadonlyArray<T> 類型,能夠確保數組建立後不能被修改
let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error!
複製代碼
  • 只讀數組不能直接賦值到一個普通數組,
a = ro; // error!
複製代碼
  • 可使用類型斷言重寫
a = ro as number[];
複製代碼

做爲變量使用的話使用 const,做爲屬性使用的話用 readonly

額外的屬性檢查

interface SquareConfig {
    color?: string;
    width?: number;
}

function createSquare(config:SquareConfig) {}
createSquare({width: 100}) 
/// 這裏咱們傳的屬性只知足其中一條可選屬性,這是沒問題的,由於color是可選的
...
createSquare({ colour: "red", width: 100 });
/// 可是若是咱們傳入一個設置以外的參數值,就會報錯
複製代碼

由於編譯器此時會進行額外的屬性檢查,colour 是額外的屬性值,接口中並無針對這個值作類型設置

解決這個問題有兩種方法:

  • 使用斷言,強行賦值
let mySquare = createSquare({ colour: "red", width: 100 } as SquareConfig);
複製代碼
  • 索引簽名
interface SquareConfig {
    color?: string;
    width?: number;
    [propName: string]: any;
}
複製代碼

這樣就表示只關注 colorwidth 的值,其餘值類型是 any,不過這種方式是不建議使用的,若是多餘的參數是必須的話,那應該對其定義

函數類型

接口除了描述帶有屬性的普通對象外,也能夠描述函數類型。

interface SearchFunc {
  (source: string, subString: string): boolean;
}
let mySearch: SearchFunc;

mySearch = function (source: string, subString: string) {
  let result = source.search(subString);
  return result > -1;
};
複製代碼

對於函數類型的類型檢查來講,函數的參數名不須要與接口裏定義的名字相匹配。

mySearch = function(src: string, sub: string): boolean {
複製代碼

可索引類型

可索引類型具備一個 索引簽名,它描述了對象索引的類型,還有相應的索引返回值類型

interface StringArray {
  [index: number]: string;
}

let myArray: StringArray;
myArray = ["Bob", "Fred"];

let myStr: string = myArray[0];
複製代碼

上例定義了 StringArray 接口,它具備索引簽名。 這個索引簽名表示了當用 number 去索引StringArray 時會獲得 string 類型的返回值

TypeScript支持兩種索引簽名:字符串和數字,能夠同時使用兩種索引,可是數字索引的返回值必須是字符串索引返回值的子類型。這是由於當使用 number 來索引時,JavaScript會將它轉換成 string而後再去索引對象。 也就是說用 100 去索引等同於使用 "100"去索引,所以二者須要保持一致。

/// 這裏能夠類比下 js 中的代碼
let ad = ['info', 'foo'];
ad[0];
ad['0']; // 索引值是數字或者字符訪問均可以
...
class Animal {
    constructor(public name:string) {}
}
class Dog extends Animal {
    constructor(public breed:string, public name:string) {
        super(name);
    }
}

interface NotOkay {
    [x: number]: Animal; // error
    [x: string]: Dog;
}
複製代碼

此時會報錯:數字索引類型「Animal」不能賦給字符串索引類型「Dog」,緣由就是說,當索引key 值使用不一樣索引簽名類型(分別爲 number、string),對應的索引返回值類型不一樣(分別爲Animal、Dog)

此時若是隻使用 numberstring 做爲索引 key 值都不會報錯

interface NotOkay {
    [x: number]: Animal;
} 
複製代碼

設置索引時,與給定的變量名無關

interface NotOkay {
    [x: number]: Animal;
    [y: string]: Dog; // Error,x y 是兩個變量名,但實際上它們表明的一個是數字索引,一個字符索引
} 
複製代碼

改成同一類型就沒有問題了

interface NotOkay {
    [x: number]: Animal;
    [x: string]: Animal;
}
複製代碼

不指定類型的屬性,默認就是 string

interface NotOkay {
    [x: string]: string;
    name: Dog; // Error name 默認就是 string
}
複製代碼

類-類型

實現接口 implements

interface ClockInterface {
    currentTime: Date;
    setTime(time:string):void;
}

class Clock implements ClockInterface {
    constructor(public currentTime:Date) { }
    setTime (item:string) {}
}
複製代碼

這麼寫限制了類 Clock ,必須實現 ClockInterface 中定義的屬性 currentTime 以及方法 setTime。

類的類型有兩部分組成:靜態部分的類型和實例的類型

接口描述的類型是類的實例部分,不會檢查類的靜態部分類型

類靜態部分與實例部分

interface ClockConstructor {
    new (hour: number, minute: number):any;
}

class Clock implements ClockConstructor {
    constructor(h: number, m: number) { }
}
複製代碼

此時會報錯,提醒你 Clock 」提供的內容與簽名「new (hour: number, minute: number): any」 不匹配,緣由是 constructor 是類的靜態部分類型,不在檢查的範圍內

要解決這些問題,能夠直接操做類的靜態部分

interface ClockConstructor {
    new (hour: number, minute: number):any;
}

interface ClockInterface {
    tick(): void;
  }

function createClock(ctor: ClockConstructor, hour: number, minute: number) {
    return new ctor(hour, minute); // 主要是理解這裏的 new 操做,至關因而把以前的靜態部分直接進行實例化了,此時就能校驗參數正確與否
}

// 對靜態部分進行類型校驗
class DigitalClock implements ClockInterface {
    constructor(h: number, m: number) {} // 靜態部分不作校驗,索引此處構造方法不一致也不會報錯
    tick() {}
}
let digital = createClock(DigitalClock, 12, 17);
/// createClock 第一個參數就進行了檢測
複製代碼

簡化些,可使用類表達式

interface ClockConstructor {
	new (hour: number, minute: number):void;
}

interface ClockInterface {
	tick():void;
}

const Clock: ClockConstructor = class Clock implements ClockInterface {
	constructor(h: number, m: number) {}
	tick() {
		console.log("beep beep");
	}
};
複製代碼

繼承接口

  • 接口具備繼承屬性,能夠將一個接口的成員複製到另外一個接口中
interface Shape {
    color: string;
}

interface Square extends Shape {
    sideLength: number;
}

let square = <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>{};
複製代碼

混合類型

接口能夠同時作爲函數和對象使用,並帶有額外的屬性

/// 下面是接口定義給函數使用
interface Counter {
    (start: number): string;
}
let counter:Counter = function(start: number) {return String(start)};
...
/// 下面是定義了給對象使用
interface Counter {
    interval: number;
    reset(): void;
}
let counter:Counter = {
    interval: 23,
    reset () {}
}
...
/// 下面是混合,即給對象又給函數使用
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;
}
複製代碼

接口繼承類

當接口繼承一個類類型時,它會繼承類的成員。接口一樣會繼承類的 private 和 protected 成員,這意味着你建立一個接口繼承了一個擁有私有或受保護的成員,這個接口類型只能被這個類或其子類所實現

class Control {
    private state: any; // state 是私有屬性
}
/// SelectableControl 繼承了 Control 有私有屬性
interface SelectableControl extends Control {
    select(): void;
}
/// TextBox 繼承至 Control 和接口沒什麼關係
class TextBox extends Control {
    select() { } // 普通類的繼承
}
/// Button 繼承至 Control 並要實現接口 因其繼承 Control,接口能夠正常實現
class Button extends Control implements SelectableControl {
    select() { } // 普通類繼承並實現接口
}

/// Image 並不是繼承至 Control,因此沒法實現這個有特殊要求的接口
class Image implements SelectableControl {
    select() { } // 錯誤:「Image」類型缺乏「state」屬性,須要實現私有屬性
}
複製代碼

基本概念 含義
類(Class) 定義了一件事物的抽象特色,包含它的屬性和方法
對象(Object) 類的實例,經過 new 生成
面向對象(OOP)的三大特性 封裝、繼承、多態
封裝(Encapsulation) 將對數據的操做細節隱藏起來,只暴露對外的接口。外界調用端不須要(也不可能)知道細節,就能經過對外提供的接口來訪問該對象,同時也保證了外界沒法任意更改對象內部的數據
繼承(Inheritance) 子類繼承父類,子類除了擁有父類的全部特性外,還有一些更具體的特性
多態(Polymorphism) 由繼承而產生了相關的不一樣的類,對同一個方法能夠有不一樣的響應。好比 Cat 和 Dog 都繼承自 Animal,可是分別實現了本身的 eat 方法。此時針對某一個實例,咱們無需瞭解它是 Cat 仍是 Dog,就能夠直接調用 eat 方法,程序會自動判斷出來應該如何執行 eat
存取器(getter & setter) 用以改變屬性的讀取和賦值行爲
修飾符(Modifiers) 修飾符是一些關鍵字,用於限定成員或類型的性質。好比 public 表示公有屬性或方法
抽象類(Abstract Class) 抽象類是供其餘類繼承的基類,抽象類不容許被實例化。抽象類中的抽象方法必須在子類中被實現
接口(Interfaces) 不一樣類之間公有的屬性或方法,能夠抽象成一個接口。接口能夠被類實現(implements)。一個類只能繼承自另外一個類,可是能夠實現多個接口

定義和使用

使用 class 定義類,使用 constructor 定義構造函數,經過 new 生成新實例的時候,會自動調用構造函數。

class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}

let greeter = new Greeter("world");
複製代碼

繼承

使用 extends 關鍵字實現繼承,子類中使用 super 關鍵字來調用父類的構造函數和方法

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();
複製代碼

存取器

使用 getter 和 setter 能夠改變屬性的賦值和讀取行爲

class animal {
    constructor (public name:string, public food:string) {}
    get eat () {
        return this.food
    }
    set eat (value) {
        this.food = value;
    }
}
複製代碼

ts中使用存取器,有亮點須要注意:

  • 編譯器輸出設置爲 ES5+
  • 只帶有 get 不帶 set 的存取器,會自動被推斷爲readonly

靜態屬性

ES7 的提案

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) { }
}
複製代碼

origin就是一個靜態屬性,訪問靜態屬性時,須要加相關類名Grid.origin,在繼承類中也如此使用

class Foo extends Grid {
    getOrigin () {
        console.log(Grid.origin.x)
    }
}

let foo = new Foo(1.2);
foo.getOrigin();
複製代碼

靜態方法

與靜態屬性相似,static 添加到 方面名前,那麼這個方法只能經過類調用

class Animal {
  static eat() {
    console.log("Animal eat food");
  }
}
Animal.eat()
let dog = new Animal();
dog.eat(); // error 靜態方法不能在實力化對象上調用
複製代碼

實例屬性

ES7 提案

class Animal {
    food = '米飯' // 在類中直接定義屬性
}
let dog = new Animal();
console.log(dog.food);
複製代碼

公有成員(默認爲public)

class Greeter {
    public greeting: string;
    public constructor(message: string) {
        this.greeting = message;
    }
    public greet() {
        return "Hello, " + this.greeting;
    }
}
複製代碼

這裏加不加 public 都可

私有成員(private)

private表示私有,私有的意思就是除了class本身以外,任何人都不能夠直接使用

class Animal {
    private name: string;
    constructor(theName: string) { this.name = theName; }
}

class Rhino extends Animal {
    constructor() { super("Rhino"); }
}

let animal = new Animal("Goat");
let rhino = new Rhino();

console.log(rhino.name); // 報錯,提醒name是私有屬性,只能在Animal中使用
複製代碼

受保護成員(protected)

protected 對於子女、朋友來講,就是 public 的,能夠自由使用,沒有任何限制

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}.`; // 這裏的this.name就是Person中受保護的成員
    }
}

let howard = new Employee("Howard", "Sales");
console.log(howard.getElevatorPitch());
console.log(howard.name); // 報錯,提醒屬性「name」受保護,只能在類「Person」及其子類中訪問
複製代碼

這個受保護還能夠用到 constructor 上,造成一種只可被繼承後實例化,不能夠直接實例化

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 是隻讀的.
複製代碼

可使用參數屬性,把聲明和賦值合併至一處

class Octopus {
    readonly numberOfLegs: number = 8;
    constructor(readonly name: string) {
    }
}
複製代碼

抽象類

  • 抽象類作爲其它派生類的基類使用
  • 抽象類是不容許被實例化的
  • 抽象類包含成員的實現細節,abstract 既能夠用於定義抽象類,也可在內部定義抽象方法,若是定義了抽象方法則要求必須在子類中實現
abstract class Animal {
    abstract makeSound(): void;
    move(): void {
        console.log('roaming the earch...');
    }
}
class Dog extends Animal { // 抽象類做爲其餘派生類的基類使用
    makeSound () {} // 抽象方法必須在子類中實現,move 方法非必須
}
let dog = new Animal(); // Error 沒法建立抽象類的實例
複製代碼

類能夠做爲實例的類型

class Octopus {
    readonly name: string | undefined;
    readonly numberOfLegs: number | undefined;
    constructor (readonly theName: string) {
        console.log(theName);
    }
}
class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}
let greeter: Greeter; // 這裏限定了 greeter的類型是一個類
greeter = new Octopus("world"); // Erroe 類型不符
複製代碼

函數

爲函數定義類型

JavaScript 中,有兩種常見的定義函數的方式:函數聲明 和 函數表達式

// 函數聲明
function add(x: number, y: number): number {
    return x + y;
}
// 函數表達式
let myAdd = function(x: number, y: number): number { return x + y; };
/// 函數中的返回值,就是函數對應的類型
複製代碼

爲函數定義類型,須要把 參數 以及 函數返回值 都考慮進來,一個完整的函數類型包含兩部分:參數類型 和 返回值類型。

let myAdd: (x: number, y: number) => number =
    function(x: number, y: number): number { return x + y; }; 
...
/// 爲了方便查看,能夠在接口中定義函數類型
interface myAddInter {
    (x: number, y: number):number // 注意此時無須使用 => 符號
}
let myAdd: myAddInter = function(x: number, y: number): number { return x + y; }; 
複製代碼

(x: number, y: number) => number這個就指定了參數類型以及函數返回類型,若是函數沒有明確的返回值,能夠指定這個值爲void。函數指定類型和函數中的參數名稱沒有必要一一對應

留意 ts 中的 => 在函數類型定義中是表示函數的定義,並不是箭頭函數

可選參數

ts 中每一個函數參數都是必須的,因此在函數調用時,多傳或者少傳參數都是不容許的,若是某些參數是非必須的,能夠設置爲可選參數

function buildName(firstName: string, lastName?: string) {
    if (lastName)
        return firstName + " " + lastName;
    else
        return firstName;
}
複製代碼

可選參數必須跟在必須參數後面。 上例 lastName 是可選參數,那麼就必須放在 firstName 的後面

默認參數

function buildName(firstName: string, lastName = "Smith") {
  console.log(`${firstName} - ${lastName}`);
}
buildName("Mr.");
buildName("Mr.", "Green");
buildName("Mr.", undefined);
/// 默認值,只有在對應值爲 undefined 纔會生效
複製代碼

默認參數無須放到參數結尾

剩餘參數

function buildName(firstName: string, ...restOfName: string[]) {
  return firstName + " " + restOfName.join(" ");
}
複製代碼

...restOfName 就是表示剩餘參數數組,在內部能夠直接使用這個參數

this 與 箭頭函數

this的值在函數被調用時纔會去指定,=> 能保存函數建立時的 this 值,而不是調用時的值

重載

是重載容許一個函數接受不一樣數量或類型的參數時,做出不一樣的處理

/// 好比這裏有兩個針對不一樣傳參類型的加法
function add (a:number, b:number) {
    return a + b;
}
...
function add (a:string, b:string) {
    return Number(a) + Number(b);
}
...
function add (a:number, b:number):number;
function add (a:string, b:string):number;
function add(a: number | string, b: number |string):any {
    if (typeof a === 'number' && typeof b === 'number') {
        return a + b;
    }
    if (typeof a === 'string' && typeof b === 'string') {
        return Number(a) + Number(b)
    }
}
...
/// 下面兩個纔是函數的重載列表
function add (a:number, b:number):number;
function add (a:string, b:string):number;
...
/// 下面是函數的實現,重載列表 中表述精確的類型,最終函數的實現只是針對全部類型的兜底
function add(a: number | string, b: number |string):any {
複製代碼

字面量類型

type Easing = "ease-in" | "ease-out" | "ease-in-out";
type numLiteral = 1 | 2 | 3 | 4;
複製代碼

泛型

泛型(Generics)是指在定義函數、接口或類的時候,不預先指定具體的類型,而在使用的時候再指定類型的一種特性,泛型只用於表示類型而不是值

/// 假定有一個函數是根據傳入參數建立數組,並返回這個建立的數組,最開始只須要建立string 類型數組
function createArray (length:number, value:string):string[] {
    let arr = [];
    for (let i = 0; i++; i < length) {
        arr[i] = value
    }
    return arr;
}
...
/// 可是一段時間以後還須要支持 number 數組,由於以前參數類型上的不一樣兩個函數功能沒有差異,這種場景就最適合使用泛型(若是使用 any 就起不到類型檢查的功能)
function createArray (length:number, value:number):number[] {
    let arr = [];
    for (let i = 0; i++; i < length) {
        arr[i] = value
    }
    return arr;
}
...
/// 泛型使用,先在函數名後添加了 <T>,其中 T 用來指代任意輸入的類型,而後在函數內就可使用 T 表示相關類型
/// T 此處是 類型變量,這種變量只用於表示類型而不是值
function createArray<T> (length:number, value:T):T[] {
    let arr = [];
    for (let i = 0; i++; i < length) {
        arr[i] = value
    }
    return arr;
}
複製代碼

定義了泛型函數後,能夠用兩種方法使用

/// 第一種是,傳入全部的參數,包含類型參數
createArray<string>(3, 'x'); // 這種調用方式,是明確指定了 T 是 string 類型
/// 第二種利用了 類型推論 編譯器會根據傳入的參數自動地幫助咱們肯定 T 的類型:
createArray(3, 4);
複製代碼

泛型參數的默認類型

function createArray<T = string> (length:number, value:T):T[] {
    let arr = [];
    for (let i = 0; i++; i < length) {
        arr[i] = value
    }
    return arr;
}
/// 能夠可泛型設定一個默認值
複製代碼

泛型約束

泛型是一個不肯定的類型,在函數內部使用泛型變量的時候,因爲事先不知道它是哪一種類型,因此不能隨意的操做它的屬性或方法

function loggingIdentity<T>(arg: T): T {
    console.log(arg.length);  // Error: 傳入類型 T 不必定有 length
    return arg;
}
...
/// 能夠指定入參爲數組
function loggingIdentity<T>(arg: T[]): T[] {
    console.log(arg.length);  // 正常,數組有length屬性
    return arg;
}
...
/// 定義數組類型也可使用 Array<> 的形式
function loggingIdentity<T>(arg: Array<T>): Array<T> {
    console.log(arg.length);  // Array has a .length, so no more error
    return arg;
}
複製代碼

除了指定入參類型,還能夠類型使用泛型約束

interface Lengthwise {
    length: number
} // 接口對應的是約束條件
/// <T extends Lengthwise> 經過 extends 來實現約束,含義是類型變量 T 繼承至 Lengthwise,其中必須包含 length 屬性
function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);  // 已經對泛型進行了約束,因此必然有 length
    return arg;
}
loggingIdentity(['2'])
複製代碼

多個類型參數之間也能夠互相約束,下面就約束 U、V 都是 T 的屬性,能保證 selElem1和 selElem2 必然存在於 obj 上

function concat<T, U extends keyof T, V extends keyof T> (obj:T, selElem1:U, selElem2:V) {
    console.log(`${obj[selElem1]} - ${obj[selElem2]}`)
}
複製代碼

泛型類型

/// 泛型函數與普通函數基本沒有什麼區別,多了一個 類型參數
function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: <T>(arg: T) => T = identity;
...
/// 還可使用帶有調用簽名的對象字面量來定義泛型函數
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;
複製代碼

還能夠把泛型參數看成整個接口的一個參數

/// GenericIdentityFn<number> 比 GenericIdentityFn 更能直觀的知道對應的類型參數是什麼
interface GenericIdentityFn<T> {
    (arg: T): T;
}

function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: GenericIdentityFn<number> = identity;
複製代碼

泛型類

泛型類使用 <> 括起泛型類型,跟在類名後面

class GenericNumber<T> {
    zeroValue: T | undefined;
    add: ((x: T, y: T) => T) | undefined
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };
複製代碼

類有兩部分:靜態部分和實例部分,泛型類指的是實例部分的類型,因此類的靜態屬性不能使用這個泛型類型。

class GenericNumber<T> {
    static standardGreeting:T; // 會提示 靜態成員不能引用類類型參數
    zeroValue: T | undefined;
    add: ((x: T, y: T) => T) | undefined
}
複製代碼

枚舉

枚舉(Enum)是一些帶名字的常量,用於取值被限定在必定範圍內的場景(好比一週七天,接口狀態碼),ts 支持數字字符串兩種枚舉。

/// 數字枚舉
/// 枚舉成員若是第一個值沒有賦值,則被賦予 0
/// 若是第一個值被賦予某項數字常量,則後續成員在上一個枚舉成員值加 1
enum Direction {
    Up = 1, // 若是不指定從0開始遞增
    Down, // 遞增爲 2
    Left, // 3
    Right // 4
}
...
/// 能夠經過枚舉屬性直接訪問枚舉成員
Direction.Down // 2
...
/// 字符串枚舉
enum Direction {
    Up = "UP",
    Down = "DOWN",
    Left = "LEFT",
    Right = "RIGHT",
}
複製代碼

枚舉定義分爲自增賦值和手動賦值兩種:自增賦值用於數字枚舉,數字按步長 1 遞增;手動賦值二者都可使用,只是若是對字符串賦值,那就要求對其後的全部值都要有初始化表達式。

/// 下面這段代碼,編譯器會提醒 Wed 後的值要設置初始化表達式(字符串沒有自增的行爲)
enum Days {Sun = 1, Mon, Tue, Wed = 'a', Thu, Fri, Sat}; 
...
/// 若是就是但願把二者混在一塊兒使用(異構枚舉,官網列出這不是一種推薦用法),能夠先定義手動賦值部分
enum Days {Wed = 'a', Sun = 1, Mon, Tue, Thu, Fri, Sat};
...
/// 初始值能夠爲小數,後續依然按步長 1 遞增
enum Days {Sun = 0.1, Mon, Tue, Wed, Thu, Fri, Sat};
複製代碼

枚舉的常數和計算所得項

枚舉成員的值能夠是常數(constant member)或計算所得項(computed member)

enum FileAccess {
    // 常數
    None,
    Read    = 1 << 1, // 使用了二元運算符
    Write   = 1 << 2,
    ReadWrite  = Read | Write, // 引用了以前定義常量以及二元運算符
    // 計算所得項
    G = "123".length
}
複製代碼

枚舉成員被看成是常數的條件 * 不具備初始化表達式而且以前的枚舉成員是常數(簡單講就是沒有賦值,前一個是一個數字常量),此時值爲上一個常量值加 1,若是這個值處於枚舉第一位,則其值爲 0 * 枚舉成員使用了常數枚舉表達式初始化(常數枚舉表達式式是 TypeScript 表達式的子集,它能夠在編譯階段求值),知足如下任意條件,就是常數枚舉表達: * 數字字面量或字符串字面量 * 引用以前定義的常量枚舉成員 * 帶括號的常數枚舉表達式 * +, -, ~ 一元運算符應用於常數枚舉表達式 * +, -, *, /, %, <<, >>, >>>, &, |, ^ 二元運算符,常數枚舉表達式作爲其一個操做對象(若求值後爲 NaN 或 Infinity,則會在編譯階段報錯)

枚舉成員類型

enum ShapeKind {
    Circle,
    Square,
}

interface Circle {
    kind: ShapeKind.Circle; // 指定接口成員 kind 類型是一個枚舉成員
    radius: number;
}

let c: Circle = { // 指定 c 的類型是 Circle
    kind: ShapeKind.Square, // 不能將類型``ShapeKind.Square``分配給類型``ShapeKind.Circle``
    radius: 100,
}
複製代碼

聯合枚舉類型

枚舉做爲類型就是每一個枚舉成員聯合

enum E {
    Foo,
    Bar,
}
// x 的類型是枚舉對象 E,它的取值只能是其中的成員
function f(x: E) {
		// Error! 判斷完 x 不爲 E.Foo 後,那麼 x 就只能爲 E.Bar ,因此這個判斷的後續沒有存在的必須
    if (x !== E.Foo || x !== E.Bar) {
        //             ~~~~~~~~~~~
    }
}
複製代碼

運行時枚舉和編譯時枚舉

ts 的代碼,能夠很直觀的分爲編譯時和運行時兩個狀態:

  • 枚舉在運行時是對象,只要類型符合能夠看成普通對象使用
enum E {
    X, Y, Z
}
function f(obj: { X: number }) {
    return obj.X;
}
f(E); // E 符合 類型 {x: number},因此能夠正常傳值
複製代碼
  • 枚舉在編譯時還不是一個真正的對象,因此在編譯時,不該該使用 keyof來獲取全部 key 值,應該使用 keyof typeof 來獲取
enum LogLevel {
    ERROR, WARN, INFO, DEBUG
}
...
/// 等同於: type LogLevelStrings = 'ERROR' | 'WARN' | 'INFO' | 'DEBUG';
type LogLevelStrings = keyof typeof LogLevel;
複製代碼

反向映射

數字枚舉成員還具有反向映射,從枚舉值到枚舉名字,字符串枚舉不支持反向映射

enum Enum {
    A
}
let a = Enum.A;
let nameOfA = Enum[a]; // "A"
複製代碼

const枚舉

普通枚舉會把代碼邏輯加到轉換後的文件中

enum Enum {
    A,
    B
}
...
/// 轉換後的代碼
"use strict";
var Enum;
(function (Enum) {
    Enum[Enum["A"] = 0] = "A";
    Enum[Enum["B"] = 1] = "B";
})(Enum || (Enum = {}));
var info = [Enum.A, Enum.B];
複製代碼

const枚舉 會避免生成額外的代碼,但要求枚舉成員只能是常數,不能是計算所得項

const enum Enum {
    A = '2',
    B = '3'
}
let info = [Enum.A, Enum.B];
...
/// 轉換後的代碼
"use strict";
var info = ["2" /* A */, "3" /* B */];
複製代碼

環境枚舉

環境枚舉(Ambient Enums)是使用 declare enum 定義的枚舉類型,declare 定義的類型只會用於編譯時的檢查,編譯結果中會被刪除

enum Enum {
    A = 1,
    B = '2222'.length
}
console.log(Enum.A);
console.log(Enum.B);
...
/// 轉換爲
"use strict";
var Enum;
(function (Enum) {
    Enum[Enum["A"] = 1] = "A";
    Enum[Enum["B"] = '2222'.length] = "B";
})(Enum || (Enum = {}));
console.log(Enum.A);
console.log(Enum.B);
複製代碼
  • 環境枚舉不容許使用計算所得項作爲枚舉成員
declare enum Enum {
    A = 1,
    B = '2222'.length // 在環境枚舉聲明中,成員初始化表達式必須是常數表達式
}
console.log(Enum.A);
console.log(Enum.B);
複製代碼
  • 在外部枚舉中,只會進行編譯時檢查,編譯後的代碼會刪除相關代碼
declare enum Enum {
    A = 1,
    B
}
console.log(Enum.A);
console.log(Enum.B);
...
/// 轉換後
"use strict";
console.log(Enum.A);
console.log(Enum.B);
複製代碼
  • 能夠和 const 配合使用,直接輸出編譯結果
declare const enum Enum {
    A = 1,
    B
}
console.log(Enum.A);
console.log(Enum.B);
...
/// 轉換爲
"use strict";
console.log(1 /* A */);
console.log(2 /* B */);
複製代碼

類型兼容性

TypeScript使用的是一種結構化的類型檢查系統 structural typing,判斷兩個類型是否兼容,只須要判斷他們的「結構」是否一致,也就是說結構屬性名和類型是否一致。

子類型(subtyping)是一種類型多態的形式。這種形式下,子類型能夠替換另外一種相關的數據類型(超類型 supertype)。若是 S 是 T 的子類型,這種子類型關係一般寫做 S <: T,意思是在任何須要使用 T 類型對象的環境中,均可以安全地使用 S 類型的對象。

通常性對象「鳥」(或超類型)引起了三個派生對象(或子類型)「鴨子」、「杜鵑」和「鴕鳥」。每一個都以本身的方式改變了基本的「鳥」的概念,但仍繼承了不少「鳥」的特徵。
/// 此處的子類型理解起來有點繞腦,我是參考維基百科舉的例子進行理解,把子類型簡單理解爲在超類基礎上進行了擴展
複製代碼

子類型與面嚮對象語言中(類或對象)的繼承是兩個概念。子類型反映了類型(即面向對象中的接口)之間的關係;而繼承反映了一類對象能夠從另外一類對象創造出來,是語言特性的實現。所以,子類型也稱接口繼承;繼承稱做實現繼承。

子類就是實現繼承,子類型就是接口繼承

(以上部份內容來源於維基百科定義)

子類型在編程語言實現方面,分爲兩種:

  • 名義子類型:在其中只有類型聲明的名字相同纔算是相同類型,子類型關係必須被顯式聲明。C, C++, C#, Java, Objective-C等語言均屬於這類(必須顯示繼承,用 extends 纔是子類型)
  • 結構子類型:兩種類型的結構組成決定了一種類型是不是另外一種類型的子類型(只要結構相同,就是子類型)
/// ts 裏的類型兼容性是基於結構子類型的
interface Named {
    name: string | undefined;
}

class Person {
    name: string | undefined;
}

let p: Named; // 定義 p 的類型是 Named
p = new Person(); // 實現時,使用Person實現,二者有相同的結構,因此類型之間兼容
複製代碼

類型兼容的基本規則

若是 x 要兼容 y(y 能夠賦值給 x),那麼 y類型 要是 x類型子類型

interface Named {
    name: string;
}

let x: Named;
let y = { name: 'Alice', location: 'Seattle' };
x = y; // y 對應的類型是 x 對應類型的子類型
...
function greet(n: Named) {
    console.log('Hello, ' + n.name);
}
greet(y); // y 對應的類型是 n 對應類型的子類型
複製代碼

函數之間的兼容規則

函數間的兼容規則,要看參數、返回值之間關係再作判斷

  • 參數不一樣,返回值類型相同
let x = (a: number) => 2;
let y = (b: number, s: string) => 0;

y = x; // OK 
x = y; // Error 不能將類型「(b: number, s: string) => number」分配給類型「(a: number) => number」
複製代碼

此時,主要看下參數列表, x 的每一個參數的類型在 y 裏找到對應類型的參數,因此能夠賦值成功(名字無所謂,類型能對上便可),可是反過來不行

  • 參數相同,返回值不一樣
let x = () => ({name: 'Alice'});
let y = () => ({name: 'Alice', location: 'Seattle'});

y = x; // Error, 不能將類型「() => { name: string; }」分配給類型「() => { name: string; location: string; }」。
x = y; // OK
複製代碼

此時,強制源函數的返回值類型必須是目標函數返回值類型子類型 x 返回值的類型是 { name: string; },y 返回值的類型是 { name: string; location: string; },y 返回值類型 是 x返回值類型 的子類型,因此 y 能賦值給 x,反過來不行

函數參數的雙變性(bivariance)

此處參考 深刻類型系統_typescript筆記8 聊聊TypeScript類型兼容,協變、逆變、雙向協變以及不變性

雙變是指同時知足協變和逆變:

interface Animal {
    base: string;
}
interface Dog extends Animal {
    addition: string;
};

// 子類型
let Animal: Animal = { base: 'base' };
let Dog: Dog = { base: 'myBase', addition: 'myAddition' };
Animal = Dog; // Dog 是 Animal 的子類型

// 協變
type Covariant<T> = T[];
let coAnimal: Covariant<Animal> = [];
let coDog: Covariant<Dog> = [];
coAnimal = coDog; // 子類能夠賦值給父類

// 逆變 --strictFunctionTypes true
type Contravariant<T> = (p: T) => void;
let contraAnimal: Contravariant<Animal> = function(p) {}
let contraDog: Contravariant<Dog> = function(p) {}
contraDog = contraAnimal; // 父類能夠賦值給子類

// 雙變
type Bivariant<T> = {
    compare(p: T): void
};
declare let biAnimal: Bivariant<Animal>;
declare let biDog: Bivariant<Dog>;
// both are ok
biDog = biAnimal;
biAnimal = biDog; // 父類子類相互賦值
複製代碼
  • 協變(covariant):容許出現父類型的地方,也容許出現子類型(子類能夠賦值給父類),即里氏替換原則

里氏替換原則(Liskov Substitution principle)是對子類型的特別定義,派生類(子類)對象能夠在程序中代替其基類(超類)對象

  • 逆變(contravariant):協變反過來,即容許出現子類型的地方,也容許出現父類型(父類能夠賦值給子類)
  • 雙變(bivariant):同時知足協變和逆變(父類,子類能夠相互賦值)
  • 不變(invariant或nonvariant):既不知足協變也不知足逆變

ts 的函數相關規則在設定時,考慮到 js 規則複雜,就把參數類型設置爲雙向協變。在比較兩個函數類型時,只要一方參數兼容另外一方的參數便可,就可以相互賦值(也就是平時咱們在使用 js 時,歷來不去考慮函數的形參與實參是否匹配的狀況,函數參數時原始數據時,只考慮個數對不對,參數是對象時,屬性值是多了仍是少了都不要緊,必要值不存在時,運行時再報錯)

可選參數和剩餘參數

比較參數兼容性時,不要求匹配可選參數,對於剩餘參數,就當成是無限多個可選參數,也不要求嚴格匹配。

枚舉

  • 來自不一樣枚舉類型的枚舉值不兼容
enum Status { Ready, Waiting };
enum Color { Red, Blue, Green };
let s = Status.Ready;
s = Color.Green;  // Error 不能將類型「Color.Green」分配給類型「Status」,兩個不一樣的枚舉類型
複製代碼
  • 數值枚舉與數值相互類型兼容
enum Status { Ready, Waiting };
// 數值兼容枚舉值
let ready: number = Status.Ready;
// 枚舉值兼容數值
let waiting: Status = 1;
複製代碼
  • 字符串枚舉並不與字符串類型相互兼容
enum Status { Ready = '1', Waiting = '0' };
let ready: string = Status.Ready;
let waiting: Status = '0'; // 報錯 不能將類型「"0"」分配給類型「Status」
複製代碼

比較兩個類類型的對象時,只有實例的成員會被比較,靜態成員和構造函數不在比較範圍內

class Animal {
    static id: string = 'Kitty';
    feet: number | undefined;
    constructor(name: string, numFeet: number) { }
  }
  
  class Size {
    feet: number | undefined;
    constructor(numFeet: number) { }
  }
  
  let a: Animal = new Animal('dog', 2);
  let s: Size = new Size(1);
  
  a = s;  // OK
  s = a;  // OK
複製代碼

泛型

對於泛型的比較,要看有沒有使用泛型參數,對於沒指定泛型類型的泛型參數時,會把全部泛型參數當成any比較

interface Empty<T> {
}
declare let x: Empty<number>;
declare let y: Empty<string>;

x = y;
...
interface NotEmpty<T> {
    data: T;
}
declare let x: NotEmpty<number>;
declare let y: NotEmpty<string>;

x = y;  // Error, 不能將類型「Empty<string>」分配給類型「Empty<number>」。
複製代碼

高級類型

交叉類型

交叉類型是將多個類型合併爲一個類型, 這讓咱們能夠把現有的多種類型疊加到一塊兒成爲一種類型,它包含了所需的全部類型的特性。

interface foo {
    foo: string
}
interface bar {
    bar: number
}
/// foo & bar 是一個交叉類型,包含 兩個類型全部特性
let baz: foo & bar = {
    foo: '',
    bar: 0
}
複製代碼

聯合類型

聯合類型表示一個值能夠是幾種類型之一,用 | 進行鏈接,聯合類型 A | B,表示類型要麼是 A 要麼是 B;

let info: string | number;
info = '';
info = 23;
info = true; // 不能將類型「true」分配給類型「string | number」。
...
interface foo {
    name: string,
    age: number
}
interface bar {
    name: string,
    flag: boolean
}
let baz: foo | bar = {
    name: '',
    age: 23
}
/// baz 的類型是 foo | bar 的聯合類型,因此 baz 的取值,要麼是 foo 類型,要麼是 bar 類型
複製代碼

若是一個值是聯合類型,咱們只能訪問此聯合類型的全部類型中共有成員

interface Bird {
    fly():void;
    layEggs():void;
}

interface Fish {
    swim():void;
    layEggs():void;
}

function getSmallPet(): (Fish | Bird) & void {
    // ...
}

let pet = getSmallPet();
pet.layEggs(); // okay
pet.swim();    // errors, 此時 ts 不能明確的確認swim方法必然存在,因此會報錯
複製代碼

類型保護

聯合類型至關於由類型構成的枚舉類型,於是沒法肯定其具體類型,因此 pet.swim() 的訪問會報錯,若是想要確切的訪問某個類的方法,可使用類型斷言

if ((<Fish>pet).swim) {
    (<Fish>pet).swim();
}
else {
    (<Bird>pet).fly();
}
複製代碼

除了使用斷言,還可使用類型保護來實現上述代碼正常工做。類型保護本質是把聯合類型從一個寬的類型,具體到一個窄的類型。

typeof 類型保護

typeof variable === 'type' 能肯定基本類型,若是用在聯合類型中,能自動縮窄對應分支下的聯合類型

/// 函數參數對應的類型是 number | string
let add = function (ad:number | string) {
		/// 這裏經過 typeof 進行了類型收窄,這裏判斷是聯合類型中的 number
    if (typeof ad === 'number') {
    
    } else {
    /// 這裏則必然是 string
        console.log(ad.length)
    }
}
複製代碼

typeof 的類型保護只能用於 numberstringbooleansymbol,針對其餘類型是不安全的

instanceof 類型保護

相似於 typeof 只不過 instanceof 使用範圍更廣

declare let x: Date | RegExp;
if (x instanceof RegExp) {
  // 正確 instanceof類型保護,自動縮窄到RegExp實例類型
  x.test('');
}
else {
  // 正確 自動縮窄到Date實例類型
  x.getTime();
}
複製代碼

instanceof 右側是個構造函數,此時左側類型會被縮窄到:

  • 該類實例的類型(構造函數 prototype 屬性的類型)
  • 由構造函數返回類型構成的聯合類型(構造函數存在重載版本時)
// Case1 該類實例的類型
declare let x:any;
if (x instanceof Date) {
  // x 從 any 縮窄到 Date
  x.getTime(); // x 類型爲 Date
}

// Case2 由構造函數返回類型構成的聯合類型
interface DateOrRegExp {
  new(): Date;
  new(value?: string): RegExp;
}

declare let A: DateOrRegExp;
let y:any;
if (y instanceof A) {
  y; // y 從 any 縮窄到 RegExp | Date
}
複製代碼

自定義類型保護

typeofinstanceof 類型保護可以知足通常場景,對於一些更加特殊的,能夠經過自定義類型保護來縮窄類型。 自定類型保護 與普通函數聲明相似,只是返回類型部分是個類型謂詞(type predicate)

interface RequestOptions {
    url: string;
    onSuccess?: () => void;
    onFailure?: () => void;
}
// 自定義類型保護,將參數類型 any 縮窄到 RequestOptions
function isValidRequestOptions(opts: any): opts is RequestOptions {
    return opts && opts.url;
}

let opts:any;
if (isValidRequestOptions(opts)) {
    // opts從any縮窄到RequestOptions
    opts.url;
}
複製代碼

opts is RequestOptions類型謂詞

!後綴類型斷言

/// ! 後綴 能夠去掉| undefined | null
let x: string | undefined | null;
x!.toUpperCase();
複製代碼

null 和 undefined

  • 檢查器默認會開啓 strictNullChecks ,當你聲明一個變量時不會自動包含 null 或 undefined
let s = "foo";
s = null; // 錯誤, 不能將類型「null」分配給類型「string」
let sn: string | null = "bar";
sn = null; // 能夠

sn = undefined; // 錯誤, 不能將類型「undefined」分配給類型「string | null」
/// tsconfig.json 中設置 "strictNullChecks": false 就能夠關閉這個檢查
複製代碼
  • ts 會把 null和 undefined區別對待。 string | nullstring | undefinedstring | undefined | null 是不一樣的類型。

  • 默認狀況下可選參數會被自動地加上 | undefined

/// 類型推導爲 f(x: number, y?: number | undefined): number
function f(x: number, y?: number) {
    return x + (y || 0);
}
f(1, 2);
f(1);
f(1, undefined);
...
/// tsconfig.json 中設置 "strictNullChecks": false 時
/// 類型推導爲 f(x: number, y?: number): number,可選參數的 undefined 就不會被加上
...
/// 是否設置 strictNullChecks 對 undefined 的使用沒有影響,影響的是 null
f(1, undefined); // 都可編譯通知
f(1, null); // 設爲 false 時編譯經過,默認狀況下提示 類型「null」的參數不能賦給類型「number | undefined」的參數
複製代碼
  • 可選屬性也會一樣處理,默認狀況下類型會添加 | undefined
class C {
    a: number; // 默認狀況,此處會報錯,屬性「a」沒有初始化表達式,且未在構造函數中明確賦值。類型推導爲 C.a: number
    b?: number; // 可選屬性的類型推導爲  C.b?: number | undefined
}
let c = new C();
c.a = 12;
c.a = undefined; // error, 不能將類型 「undefined」 分配給類型 「number」
c.b = 13;
c.b = undefined; // ok
...
/// tsconfig.json 中設置 "strictNullChecks": false 時,上面報錯的就不會再報錯
複製代碼

類型別名

類型別名能爲現有類型建立一個別名,從而加強其可讀性

type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName(n: NameOrResolver): Name {
    if (typeof n === 'string') {
        return n;
    }
    else {
        return n();
    }
}
複製代碼

類型別名在定義和使用上和接口很接近

interface Animal {
    name: string
}
interface Dog extends  Animal {
    age: number
}
...
type Animal = {
    name: string
}
type Dog = Animal & {
    age: number
}
複製代碼

與接口的區別在於:

  • 類型別名並不會建立新類型,而接口會定義一個新類型
type Alias = { num: number }
interface Interface {
    num: number;
}
declare function aliased(arg: Alias): Alias;
declare function interfaced(arg: Interface): Interface;
複製代碼

鼠標放到Alias上看到的信息是

/// Alias 只是一個集合,並非新的類型
type Alias = {
    num: number;
}
複製代碼

也就是其定義的內容,若是放到 Interface

/// Interface 是一個新的類型
interface Interface
複製代碼
  • 容許給任意類型起別名,但沒法給任意類型定義與之等價的接口(好比基礎類型)
  • 沒法繼承或實現類型別名(也不能擴展或實現其它類型),但接口能夠
  • 類型別名能將多個類型組合成一個具名類型,而接口沒法描述這種組合(交叉、聯合等)
interface Animal {
    name: string
}
interface Dog extends  Animal {
    age: number
}
class erha implements Dog {
    constructor (public name:string, public age:number) {}
}
...
/// 類型別名能夠被繼承並生成新的交叉類型
type Animal = {
    name: string
}
type Dog = Animal & {
    age: number
}
class erha implements Dog {
    constructor (public name:string, public age:number) {}
}
複製代碼
  • 若是沒法經過接口來描述一個類型而且須要使用聯合類型或元組類型時,這時能夠考慮使用類型別名

應用場景上,兩者區別以下:

  • 接口:OOP場景(由於能被繼承和實現,維持着類型層級關係)
  • 類型別名:追求可讀性的場景、接口沒法描述的場景(基礎類型、交叉類型、聯合類型等)

字符串字面量

字符串字面量類型容許你指定字符串必須的固定值

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") {
        }
    }
}

let button = new UIElement();
button.animate(0, 0, "ease-in");
button.animate(0, 0, "uneasy"); // error 類型「"uneasy"」的參數不能賦給類型「Easing」的參數。
複製代碼

字符串字面量類型還能夠用於區分函數重載

function createElement(tagName: "img"): HTMLImageElement;
function createElement(tagName: "input"): HTMLInputElement;
// ... more overloads ...
function createElement(tagName: string): Element {
    // ... code goes here ...
}
複製代碼

數字字面量類型

type num = 1 | 2 | 3 | 4;
let nm1: num = 3;
複製代碼

枚舉與字面量

/// 聯合枚舉
enum E {
  Foo,
  Bar
}

/// 枚舉類型
function f(x: E) {
  if (x !== E.Foo) {
  }
}
...
/// 字面量
function f(x: 'Foo' | 'Bar') {
	if (x !== E.Foo) {
  }
}
複製代碼

這裏用字符串字面量聯合類型 'Foo' | 'Bar' 模擬枚舉 E,從類型角度來看,聯合枚舉就是由數值/字符串字面量構成的枚舉,聯合枚舉,即數值/字符串聯合

枚舉成員類型與數值/字符串字面量類型也叫單例類型

一個單例類型下只有一個值,例如字符串字面量類型'Foo'只能取值字符串'Foo'

可區分聯合(Discriminated Unions)

結合單例類型,聯合類型,類型保護和類型別名可建立一個叫作 可區分聯合 高級模式,它也稱作 標籤聯合代數數據類型,其可運算、可進行邏輯推理的類型,一個可區分聯合具備如下三部分組成:

  • 具備公共單例類型屬性的類型——公共單例屬性便可區分的特徵(或者叫標籤)
  • 一個指向這些類型構成的聯合的類型別名——即聯合
  • 針對公共屬性的類型保護
/// 一些具備公共單例屬性(kind)的類型,``kind`` 屬性稱作可辨識的特徵或標籤,其它的屬性則特定於各個接口,目前各個接口之間沒有聯繫
interface Square {
    kind: "square";
    size: number;
}
interface Rectangle {
    kind: "rectangle";
    width: number;
    height: number;
}
interface Circle {
    kind: "circle";
    radius: number;
}
...
/// 定義聯合類型,並起個別名
type Shape = Square | Rectangle | Circle;
複製代碼

使用可區分聯合,經過區分公共單例屬性的可縮窄父類型,達到類型保護的能力:

function area(s: Shape) {
    switch (s.kind) {
    		/// 自動縮窄到 "square"
        case "square": return s.size * s.size;
        /// 自動縮窄到 "rectangle"
        case "rectangle": return s.height * s.width;
        /// 自動縮窄到 "circle"
        case "circle": return Math.PI * s.radius ** 2;
    }
}
複製代碼

與 instanceof類型保護 的區別:

  • instanceof類型保護:適用於有明確繼承關係的父子類型
  • 可區分聯合類型保護:適用於沒有明確繼承關係(運行時經過instanceof檢測不出繼承關係)的父子類型

完整性檢查

/// 修改類型別名,添加 Triangle
type Shape = Square | Rectangle | Circle | Triangle; 

function area(s: Shape) {
    switch (s.kind) {
    		/// 自動縮窄到 "square"
        case "square": return s.size * s.size;
        /// 自動縮窄到 "rectangle"
        case "rectangle": return s.height * s.width;
        /// 自動縮窄到 "circle"
        case "circle": return Math.PI * s.radius ** 2;
        /// 此處遺漏了 對 Triangle 的類型處理,
    }
}
複製代碼

當沒有涵蓋全部可辨識的變化,可使用兩種方式進行完整性檢查

  • 啓用 --strictNullChecks 而且指定一個返回值類型,函數調用默認會返回 undefined ,若是指定了返回類型,沒有處理的分支,就會返回 undefined ,這樣編譯階段就能保證完整性

"strictNullChecks": true

function area(s: Shape): number { // error: 函數缺乏結束返回語句,返回類型不包括 "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;
    }
}
複製代碼
  • 第二種使用 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 類型「Triangle」的參數不能賦給類型「never」的參數。
    }
}
複製代碼

這裏, assertNever 檢查 s 是否爲 never 類型—即爲除去全部可能狀況後剩下的類型。

this 類型

/// JavaScript
class A {
  foo() { return this }
}
class B extends A {
  bar() { return this }
}

new B().foo().bar(); 
/// 在 JavaScript 運行時經過 this 造成鏈式調用
...
/// A類實例類型 類型推導 (method) A.foo(): A,foo 方法對應的 this 爲 A
new A().foo();
/// B類實例類型 類型推導 (method) A.foo(): B,foo 方法對應的 this 爲 B
new B().foo();
/// this 的類型並非固定的,取決於其調用上下文
複製代碼

此時若是在 ts 中針對 foo 或 bar 添加類型的話,大概只能是 B & A 這種,這並非很合理的類型,針對這種場景,ts 引入了 this類型,this 表示所屬類或接口的子類型 ,這被稱爲 有界多態性(F-bounded polymorphism),它能很容易的表現連貫接口間的繼承。

class BasicCalculator {
    public constructor(protected value: number = 0) { }
    public currentValue(): number {
        return this.value;
    }
    /// 類型推導爲 (method) BasicCalculator.add(operand: number): this
    public add(operand: number): this {
        this.value += operand;
        return this;
    }
    /// 類型推導爲 (method) BasicCalculator.multiply(operand: number): this
    public multiply(operand: number) {
        this.value *= operand;
        return this;
    }
    // ... other operations go here ...
}
複製代碼

好比, 在上面計算器的例子裏,在每一個操做以後都返回 this 類型(this 類型可以自動對應到所屬類實例類型)

這種JavaScript運行時特性,在TypeScript靜態類型系統中一樣支持

/// 因爲調用時返回了this 類型,此時可使用鏈式調用
let v = new BasicCalculator(2)
            .multiply(5)
            .add(1)
            .currentValue();
複製代碼

新的類能夠直接使用以前的方法,不須要作任何的改變

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();
複製代碼

TypeScript中的this類型分爲2類:

  • class this type:類/接口(的成員方法)中的 this 類型
  • function this type:普通函數中的 this 類型

function this type

/// 把this顯式地做爲函數的(第一個)參數,從而限定其類型,像普通參數同樣進行類型檢查
declare class C { m(this: this):void; }
let c = new C();
// f 類型爲 (this:C) => any
let f = c.m;
// 錯誤 類型爲「void」的 "this" 上下文不能分配給類型爲「C」的方法的 "this"
f();
...
/// 去掉顯式聲明的 this類型
declare class C { m(); }
...
/// 正確
f(); 
複製代碼

箭頭函數(lambda)的 this 沒法手動限定其類型

索引類型(Index types)

索引類型讓靜態檢查可以覆蓋到類型不肯定(沒法窮舉)的」動態「場景

/// pluck 函數能從 o 中摘出來 names 指定的那部分屬性
function pluck(o, names) {
  return names.map(n => o[n]);
}
複製代碼

此處編譯時,會報錯。須要添加約束,這裏須要兩個約束條件:

  • 參數 names 只能是 o 的屬性
  • 返回類型是參數 o 身上屬性值的類型
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[]
複製代碼
  • keyof:索引類型查詢操做符(index type query operator)
  • T[K]:索引訪問操做符(indexed access operator)

keyof T 取類型 T 上的全部 public 屬性名構成聯合類型

經過 索引類型查詢 和 索引訪問操做符使用,編譯器會檢查 name 是否真的是 Person 的一個屬性

keyof Person 是徹底能夠與 'name' | 'age' 互相替換的。 不一樣的是若是你添加了其它的屬性到 Person,例如 address: string,那麼 keyof Person 會自動變爲 'name' | 'age' | 'address'

keyof 在沒法得知(或沒法窮舉)屬性名的場景頗有意義

T[K] 索引訪問操做符,直接反應相關類型,這個要確保 K extends keyof T 被正常調用,那麼索引訪問操做符就能夠正常使用,好比 person['name'] 就表示 string 類型

keyof 與 T[K] 一樣適用於字符串索引簽名(index signature)

/// 若是是一個帶有字符串索引簽名的類型,那麼keyof T 會是string | number
interface NetCache {
  [propName: string]: object;
}

/// keyType: string | number 如按定義,此處類型應爲 string,只不過由於在JavaScript裏的數值索引會被轉換成字符串索引,好比 arr[0] === arr['0'],因此對應的類型能夠是字符串也能夠是數值
let keyType: keyof NetCache;
...
/// 若是一個類型帶有數字索引簽名,那麼keyof T爲number
interface NetCache {
	[propName: number]: object;
}

// keyType: number
let keyType: keyof NetCache;
複製代碼

映射類型

這是一種從舊類型建立新類型的方式,在映射類型裏,新類型以相同的形式去轉換舊類型裏的每一個屬性。

interface Person {
    name: string;
    age: number
}
type PersonPartial = Partial<Person>;
type ReadonlyPerson = Readonly<Person>;
...
/// 類型別名 Readonly Partial 存在於 lib.es5.d.ts 
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
}
type Partial<T> = {
    [P in keyof T]?: T[P];
}
複製代碼

這樣就由一箇舊類型建立了一個新的類型,上面例子中針對源類型信息都保留了,僅存在修飾符上的差別,這類轉換被稱爲同態轉換

type Keys = 'option1' | 'option2';
/// type Flags = { option1: boolean; option2: boolean; }
type Flags = { [K in Keys]: boolean };
複製代碼

[K in Keys] 形式上與索引簽名相似,只是融合了 for...in 語法,具備三個部分:

  • K:類型變量,依次綁定到每一個屬性上,對應每一個屬性名的類型
  • Keys:字符串字面量構成的聯合類型,表示一組屬性名(的類型)
  • boolean:映射結果類型,即每一個屬性值的類型 Flags 丟棄了源屬性值類型,屬於非同態(non-homomorphic)轉換,非同態類型本質上會建立新的屬性,它們不會從它處拷貝屬性修飾符

映射類型描述的是類型而非成員, 若想添加成員,則可使用交叉類型

type Flags = { [K in Keys]: boolean; } & { newMember: boolean };
複製代碼

條件類型

條件類型用來表達非均勻類型映射(non-uniform type mapping),可以根據類型兼容關係(即條件)從兩個類型中選出一個

T extends U ? X : Y
複製代碼
/// 當即解析
declare function f<T extends boolean>(x: T): T extends true ? string : number;

// Type is 'string | number let x = f(Math.random() < 0.5) ... /// 推遲解析 interface Foo { propA: boolean; propB: boolean; } declare function f<T>(x: T): T extends Foo ? string : number; function foo<U>(x: U) { // a 的類型爲 U extends Foo ? string : number,當有另外一段代碼調用foo,它會用其它類型替換U,TypeScript將從新計算有條件類型,決定它是否能夠選擇一個分支 let a = f(x); let b: string | number = a; } ... // 嵌套的條件類型相似於模式匹配 type TypeName<T> = T extends string ? "string" : T extends number ? "number" : T extends boolean ? "boolean" : T extends undefined ? "undefined" : T extends Function ? "function" : "object"; 複製代碼

可分配條件類型

可分配條件類型(distributive conditional type)中被檢查的類型是個裸類型參數(naked type parameter)。其特殊之處在於知足分配律

(A | B | C) extends U ? X : Y
等價於
(A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)
複製代碼
type Diff<T, U> = T extends U ? never : T;

/// T 類型等價於聯合類型 "b" | "d"
/// 等價於
/// 'a' extends 'a' | 'c' | 'f' ? never : 'a'
/// 'b' extends 'a' | 'c' | 'f' ? never : 'b'
/// 'c' extends 'a' | 'c' | 'f' ? never : 'c'
/// 'd' extends 'a' | 'c' | 'f' ? never : 'd'
type T = Diff<"a" | "b" | "c" | "d", "a" | "c" | "f">;
複製代碼

預約義的有條件類型

  • Exclude<T, U> -- 從T中剔除能夠賦值給U的類型。
  • Extract<T, U> -- 提取T中能夠賦值給U的類型。
  • NonNullable<T> -- 從T中剔除null和undefined。
  • ReturnType<T> -- 獲取函數返回值類型。
  • InstanceType<T> -- 獲取構造函數類型的實例類型。

模塊

若是一個文件中含有合法的 importexport 語句,就會被當作模塊(擁有模塊做用域),不然就將在運行在全局做用域下。

導出

任何聲明(好比變量,函數,類,類型別名或接口)都可以經過添加 export 關鍵字來導出

/// index.ts
export interface Animal {
    name: string;
    age: number;
}
...
/// main.ts
import { Animal } from './index'
export const dog:Animal = {
    name: '',
    age: 0
}
export class cat implements Animal {
    constructor (public name:string, public age:number) {}
}
複製代碼

導入

import/require 會引入目標模塊源碼,並從中提取類型信息

import { name } from './file';
複製代碼

轉編碼時要指定module

tsc --module commonjs
...
/// 或者在 tsconfig.json 文件中設置 module
"target": "ES5" /* 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */,
"module": "commonjs" /* 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
複製代碼
  • commonjs NodeJS模塊定義
  • amd AMD (Require.js )
  • system SystemJS
  • umd UMD
  • es6 ES Module
  • es2015 等價於es6
  • esnext 還沒有收入ES規範的前沿模塊定義,如import(), import.meta
  • none 禁用全部模塊定義,如import, export等(用到的話會報錯)

上面模塊系統中,除了 es module 和 commonjs 其餘已經不怎麼使用

CommonJS

// NodeJS模塊(CommonJS)
let x = {a: 1};
exports.x = x;
module.exports = x;
複製代碼

針對 CommonJS 的 exports 語法,ts 提供了 export =,對應的導入 import module = require("module")

/// index.ts
let x = { a: 1 };
export = x;
...
/// main.ts
import index = require("./index");
複製代碼

使用其它的JavaScript庫

要想描述非 TypeScript 編寫的類庫的類型,咱們須要聲明類庫所暴露出的 API,在聲明文件(d.ts)裏定義的,進行補充類型聲明

聲明文件

當使用第三方庫時,咱們須要引用它的聲明文件,才能得到對應的代碼補全、接口提示等功能

語法 含義
declare var 聲明全局變量
declare function 聲明全局方法
declare class 聲明全局類
declare enum 聲明全局枚舉類型
declare namespace 聲明(含有子屬性的)全局對象
interface 和 type 聲明全局類型
export 導出變量
export namespace 導出(含有子屬性的)對象
export default ES6 默認導出
export = commonjs 導出模塊
export as namespace UMD 庫聲明全局變量
declare global 擴展全局變量
declare module 擴展模塊
/// <reference /> 三斜線指令

聲明語句

當 ts 文件引入 第三方庫時,第三方庫可能會暴露出一些全局變量,好比 $、jQuery,或者是爲這些變量設定類型,能夠經過declare var進行聲明。

declare var 並無真的定義一個變量,只是定義了全局變量 jQuery 的類型,僅僅會用於編譯時的檢查,在編譯結果中會被刪除

declare const jQuery: (selector: string) => any;

jQuery('#foo');
...
/// 編譯爲
"use strict";

jQuery("#foo");
複製代碼

聲明文件

把聲明語句放到一個單獨的文件就是聲明文件,聲明文件必需以 .d.ts 爲後綴

/// 單獨文件 jQuery.d.ts
declare const jQuery: (selector: string) => any;
...
/// index.ts
jQuery("#foo");
複製代碼

ts 會解析項目中全部的 *.ts 文件,當咱們將 jQuery.d.ts 放到項目中時,其餘全部 *.ts 文件就均可以得到 jQuery 的類型定義了

若沒法解析,能夠檢查下 tsconfig.json 中的 files、include 和 exclude 配置,確保其包含了 jQuery.d.ts 文件

針對 jQuery 是沒有必要這樣單獨定義的,能夠直接使用第三方聲明文件 @types/jquery

書寫聲明文件

庫的使用場景主要有如下幾種:

  • 全局變量:經過 <script> 標籤引入第三方庫,注入全局變量
  • npm 包:經過 import foo from 'foo' 導入,符合 ES6 模塊規範
  • UMD 庫:既能夠經過 <script> 標籤引入,又能夠經過 import 導入
  • 直接擴展全局變量:經過 <script> 標籤引入後,改變一個全局變量的結構
  • 在 npm 包或 UMD 庫中擴展全局變量:引用 npm 包或 UMD 庫後,改變一個全局變量的結構
  • 模塊插件:經過 <script>import 導入後,改變另外一個模塊的結構

全局變量

全局變量的聲明文件主要有如下幾種語法:

  • declare var/let/const 聲明全局變量
  • declare function 聲明全局方法
  • declare class 聲明全局類
  • declare enum 聲明全局枚舉類型
  • declare namespace 聲明(含有子屬性的)全局對象
  • interface 和 type 聲明全局類型
declare var/let/const

定義一個全局變量的類型,全局變量都是禁止修改的常量,因此大部分狀況都應該使用 const 而不是 var 或 let

declare const jQuery: (selector: string) => any;
複製代碼

聲明語句中只能定義類型,切勿在聲明語句中定義具體的實現

/// Error 不容許在環境上下文中使用初始化表達式
declare const jQuery = function(selector) {
    return document.querySelector(selector);
};
複製代碼

declare function

定義全局函數的類型

declare function jQuery(selector: string): any;

jQuery("#foo");
複製代碼

在函數類型的聲明語句中,函數重載也是支持的

declare function jQuery(selector: string): any;
declare function jQuery(domReadyCallback: () => any): any;

jQuery('#foo');
jQuery(function() {
    alert('Dom Ready!');
});
複製代碼

declare class

當全局變量是一個類的時候,使用 declare class 定義類型。declare class 語句也只能用來定義類型,不能用來定義具體的實現,

declare class Animal {
    name: string;
    constructor(name: string);
    sayHi(): string;
}

let cat = new Animal('Tom');
cat.sayHi()
複製代碼

declare enum

使用 declare enum 定義的枚舉類型也稱做外部枚舉 (Ambient Enums)

declare enum Directions {
  Up,
  Down,
  Left,
  Right
}

let directions = [
  Directions.Up,
  Directions.Down,
  Directions.Left,
  Directions.Right
];
...
/// 編譯爲
"use strict";
var directions = [
    Directions.Up,
    Directions.Down,
    Directions.Left,
    Directions.Right
];
/// 其中 Directions 是由第三方庫定義好的全局變量
複製代碼

declare namespace

namespace 是 ts 早期時爲了解決模塊化而創造的關鍵字,中文稱爲命名空間,因爲 ES module 的普遍使用,ts 的命名空間已經沒有使用的意義。

declare namespace 用來表示全局變量是一個對象,包含不少子屬性,適用於在全局變量還有子屬性的狀況

declare function jQuery(selector: string): any;
declare namespace jQuery {
    function ajax(url: string, settings?: any): void;
}

jQuery.ajax('/api/get_something');
複製代碼

若是對象擁有深層的層級,則須要用嵌套的 namespace 來聲明深層的屬性的類型

declare namespace jQuery {
    namespace fn {
        function extend(object: any): void;
    }
}

jQuery.fn.extend({
    check: function() {}
});
複製代碼

interface 和 type

能夠直接使用 interface 或 type 來聲明一個全局的接口或類型

/// jQuery.d.ts
interface AjaxSettings {
    method?: 'GET' | 'POST'
    data?: any;
}
declare namespace jQuery {
    function ajax(url: string, settings?: AjaxSettings): void;
}
...
/// index.ts
let settings: AjaxSettings = {
    method: 'POST',
    data: {
        name: 'foo'
    }
};
jQuery.ajax('/api/post_something', settings);
複製代碼

jQuery.d.ts 中,AjaxSettings 暴露在命名中間以外,做爲全局類型做用於整個項目中,咱們應該儘量的減小全局變量或全局類型的數量,最好將他們放到 namespace 下

declare namespace jQuery {
    interface AjaxSettings {
        method?: 'GET' | 'POST'
        data?: any;
    }
    function ajax(url: string, settings?: AjaxSettings): void;
}
...
/// 使用時加上 jQuery 前綴,jQuery.AjaxSettings
let settings: jQuery.AjaxSettings = {
複製代碼

聲明合併

多個聲明語句會合並起來

declare function jQuery(selector: string): any;
declare namespace jQuery {
    function ajax(url: string, settings?: any): void;
    namespace fn {
        function extend(object: any): void;
    }
}
複製代碼

npm包

若是咱們使用是一個 npm 包,能夠先看看對應的包有沒有聲明文件(能夠在 npm官網搜索 @types),好比@types/node@types/react@types/jest

沒有的話,能夠自行編寫聲明文件,因爲是經過 import 語句導入的模塊,因此聲明文件存放的位置也有所約束,有兩種方式:

  • 建立一個 node_modules/@types/xx/index.d.ts 文件,存放 xx 模塊的聲明文件。
  • 建立一個 types 目錄,專門用來管理本身寫的聲明文件,將 xx 的聲明文件放到 types/xx/index.d.ts 中。這種方式須要配置下 tsconfig.json 中的 pathsbaseUrl 字段。
/// 目錄結構
/path/to/project
├── src
|  └── index.ts
├── types
|  └── xx
|     └── index.d.ts
└── tsconfig.json
...
/// tsconfig.json
{
    "compilerOptions": {
        "module": "commonjs",
        "baseUrl": "./",
        "paths": {
            "*": ["types/*"]
        }
    }
}
複製代碼

這樣配置後,若是導入 xx 就會先到 types 下尋找有沒有聲明文件

/// types/foo/index.d.ts
export const name: string;
export function getName(): string;
...
/// index.d.ts
import { name, getName } from 'foo';

/// (alias) const name: string
/// import name
console.log(name); // 這樣聲明文件能夠正常使用
getName();
複製代碼

npm 包聲明方式

export 導出變量
  • export 的語法與普通的 ts 中的語法相似,區別在於聲明文件中禁止定義具體的實現
/// types/foo/index.d.ts 
/// 不容許在環境上下文中使用初始化表達式
export const name: string = '';
const age: number;
複製代碼
  • 在 npm 包的聲明文件中,使用 declare 再也不會聲明一個全局變量,而只會在當前文件中聲明一個局部變量。只有在聲明文件中使用 export 導出,而後在使用方 import 導入後,纔會應用到這些類型聲明
/// types/foo/index.d.ts
export const name: string;
declare const age: number;
export { age };
...
/// index.d.ts
/// (alias) const name: string
/// (alias) const age: number
import { name, age } from 'foo';
複製代碼
  • export namespace 用來導出一個擁有子屬性的對象
/// types/foo/index.d.ts
export namespace foo {
  const name: string;
  namespace bar {
    function baz(): string;
  }
}
...
/// index.d.ts
import { foo } from 'foo';

console.log(foo.name);
foo.bar.baz();
複製代碼
  • export default 能夠導出一個默認值
// types/foo/index.d.ts
export default function foo(): string;
...
// src/index.ts
import foo from 'foo';
foo();
複製代碼

只有 function、class 和 interface 能夠直接默認導出,其餘的變量須要先定義出來,再默認導出

  • export = commonjs 導出模塊,不建議使用

命名空間

在早期 js 版本,還沒有引入模塊概念時,會有一種經過匿名函數向現有對象添加內容或建立對象的功能,達成相似命名空間的能力(這種方式能夠確保建立的變量不會泄漏至全局變量上)

var something;
(function(something) {
  something.foo = 123;
})(something || (something = {}));

console.log(something); // { foo: 123 }

(function(something) {
  something.bar = 456;
})(something || (something = {}));

console.log(something); // { foo: 123, bar: 456 }
複製代碼

後來隨着es module的興起,這種寫法已經沒人用了,不過 ts 中還保留着,能夠做爲一種組織代碼的手段(按官網的說法以前是叫內部模塊,但由於怕和模塊系統搞混,因此改叫命名空間)

namespace something {
  export let foo:number = 123;
  export let bar:number = 456;
}
console.log(something); // { foo: 123, bar: 456 }

複製代碼

別名

是一種簡化命名空間操做的方法,使用import q = x.y.z 給經常使用對象起一個短的名字

namespace something {
  export let foo = 123;
  export let bar = 456;
}
import foo = something.foo;
console.log(foo);
複製代碼

命名空間會存在難以識別組件之間的依賴關係的致命問題,因此除非是移植舊的 js 代碼,通常狀況下不建議使用,對於新項目來講推薦使用模塊作爲組織代碼的方式。

模塊

ts 兼容 ES Module 規範,簡單來說,若是一個文件中含有合法的import或export語句,就會被當作模塊(擁有模塊做用域,在這個文件中建立一個本地的做用域)。

文件模塊在ts中也被稱爲外部模塊,徹底兼容 ES Module 語法

/// index.ts
export const foo = "foo";
...
/// main.ts
import { foo } from "./index";
const bar = foo;
複製代碼

模塊解析

通常狀況下,import/require 會引入目標模塊源碼,並從中提取類型信息,因爲導入路徑不一樣,因此存在兩種大相徑庭的模塊(模塊的解析主要是對路徑的解析):

  • 相對導入模塊(路徑以 . 開頭,例如:./someFile 或者 ../../someFolder/someFile 等);
  • 動態查找模塊(如:core-jstypestylereact 或者甚至是 react/core 等)

模塊解析策略

模塊解析策略:Node和Classis

可使用 --moduleResolution 標記來指定使用哪一種模塊解析策略。若未指定,那麼在使用了 --module AMD | System | ES2015 時的默認值爲Classic,其它狀況時則爲Node

Classic 解析方式

相對導入模塊

相對導入模塊是相對於導入它的文件進行解析,

/// root/src/folder/A.ts
import { b } from "./moduleB 複製代碼

這裏的查找 moduleB 的流程爲

  • /root/src/folder/moduleB.ts
  • /root/src/folder/moduleB.d.ts
動態查找模塊

動態查找模塊進行解析時,編譯器會從包含導入文件的目錄開始依次向上級目錄遍歷,嘗試定位匹配的聲明文件。

/// root/src/folder/A.ts
import { b } from "moduleB"
複製代碼

會按以下的順序來查找"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 解析方式

相對導入模塊
/// root/src/moduleA.js
var x = require("./moduleB");
複製代碼

如下面的順序解析這個導入:

  • 檢查/root/src/moduleB.js 文件是否存在。
  • 檢查/root/src/moduleB 目錄是否包含一個package.json 文件,且package.json 文件指定了一個"main" 模塊。若是發現文件 /root/src/moduleB/package.json 包含了 { "main": "lib/mainModule.js" },那麼Node.js會引用/root/src/moduleB/lib/mainModule.js
  • 檢查/root/src/moduleB 目錄是否包含一個index.js文件。 這個文件會被隱式地看成那個文件夾下的"main"模塊。

解析思路和classic實際上是同樣的,只不過會多一些對 package 以及目錄的處理

動態查找模塊

Node 會在一個特殊的文件夾 node_modules 裏查找你的模塊。 node_modules 可能與當前文件在同一級目錄下,或者在上層目錄裏。 Node會向上級目錄遍歷,查找每一個 node_modules 直到它找到要加載的模塊。

/// root/src/moduleA.js
var x = require("moduleB");
複製代碼

會如下面的順序去解析 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_modules 進行

TypeScript解析模塊

ts 是模仿 Node 運行時的解析策略來在編譯階段定位模塊定義文件。

ts 在 Node 解析邏輯基礎上增長了 ts 源文件的擴展名( .ts.tsx.d.ts)。 同時,ts 在 package.json裏使用字段"types"來表示相似"main"的意義 - 編譯器會使用它來找到要使用的"main"定義文件。(至關於模擬 NodeJS 的main字段)

大致按以下步驟:

  • 先嚐試尋找模塊對應的文件(.ts/.tsx)
  • 若是沒有找到,而且不是相對模塊引入(non-relative),就嘗試尋找外部模塊聲明(ambient module declaration),即 d.ts
  • 若是還沒找到,報錯 Cannot find module

相對導入模塊

/// /root/src/moduleA.ts
import { b } from "./moduleB 複製代碼

會如下面的流程來定位"./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

動態查找模塊

/// /root/src/moduleA.ts
import { b } from "moduleB"
複製代碼

會如下面的流程來定位"./moduleB",流程與 node 解析相似,,只是會額外地從node_modules/@types 裏尋找 d.ts 聲明文件

  • /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/@types/moduleB.d.ts

  • /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/@types/moduleB.d.ts

  • /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/@types/moduleB.d.ts

  • /node_modules/moduleB/index.ts

  • /node_modules/moduleB/index.tsx

  • /node_modules/moduleB/index.d.ts

附加模塊的解析

ts 構建時會把 .ts 編譯成 .js,並從不一樣的源位置把依賴拷貝到同一個輸出位置。所以,在運行時模塊可能具備不一樣於源文件的命名,或者編譯時最後輸出的模塊路徑與對應的源文件不匹配,爲了不路徑混亂的問題,ts 提供了一系列標記用來告知編譯器指望發生在源路徑上的轉換,以生成最終輸出

tsconfig.json 能夠指定 baseUrlpaths 來指導編譯器查找須要導入的模塊

{
  "compilerOptions": {
    "baseUrl": ".", // This must be specified if "paths" is.
    "paths": {
      "jquery": ["node_modules/jquery/dist/jquery"] // 此處映射是相對於"baseUrl"
    }
  }
}
複製代碼

Base Url

設置 baseUrl 告訴編譯器到哪裏去查找模塊,全部動態查找模塊的導入都會被當作相對於 baseUrl

ts 在解析動態查找模塊時,是相對於 baseUrl 或根據 paths 尋找模塊

baseUrl的值由如下二者之一決定:

  • 命令行中 baseUrl 的值(指定相對路徑的話,根據當前目錄計算)
  • tsconfig.json 裏的 baseUrl 屬性(相對路徑的話,根據 tsconfig.json 所在目錄計算)

相對模塊的導入不會被設置的 baseUrl 所影響,由於它們老是相對於導入它們的文件

paths

paths 是相對於 baseUrl 進行解析。若是 "baseUrl" 被設置成了除 "." 外的其它值,好比 tsconfig.json 所在的目錄,那麼映射必需要作相應的改變。 若是你在上例中設置了 "baseUrl": "./src",那麼jquery應該映射到"../node_modules/jquery/dist/jquery"

由於此時的基本路徑爲 /src/ ,而 node_modules 的目錄實際上是在上一層目錄

經過path 能夠指定多個備選位置(也有翻譯成 回退位置),好比下面的工程

/// 假設在一個工程配置裏,有一些模塊位於一處,而其它的則在另個的位置。 構建過程會將它們集中至一處
projectRoot
├── folder1
│   ├── file1.ts (imports 'folder1/file2' and 'folder2/file3')
│   └── file2.ts
├── generated
│   ├── folder1
│   └── folder2
│       └── file3.ts
└── tsconfig.json
複製代碼

對應配置文件

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "*": [
        "*",
        "generated/*"
      ]
    }
  }
}
複製代碼

這個配置的意思以下:

  • "*": 表示名字不發生改變,<moduleName> 路徑映射爲 <baseUrl>/<moduleName>
  • "generated/*": 表示模塊名添加了 「generated」 前綴,<moduleName>路徑映射爲<baseUrl>/generated/<moduleName>

file1.ts 導入文件folder1/file2folder2/file3過程以下

  • 導入 folder1/file2
    • 嘗試經過* 進行匹配
      • 與baseUrl合併查詢projectRoot/folder1/file2.ts是否存在
      • 存在,完成
  • 導入 folder2/file3
    • 嘗試經過 * 進行匹配
      • 與baseUrl合併查詢projectRoot/folder2/file3是否存在
      • 不存在,跳出
    • 嘗試經過 generated/* 進行匹配
      • 與baseUrl合併查詢projectRoot/generated/folder2/file3.ts 是否存在
      • 存在,完成

利用rootDirs 指定虛擬目錄

若是多個目錄下的工程源文件在編譯時合併在某個目錄一併輸出,這種行爲能夠認爲是這些源目錄建立了一個虛擬 目錄,利用rootDirs 能夠指定這個虛擬目錄

好比有這樣的工程

src
 └── views
     └── view1.ts (imports './template1')
     └── view2.ts

 generated
 └── templates
         └── views
             └── template1.ts (imports './view2')
複製代碼

這裏假設構建工具會把它們整合到同一輸出目錄中(也就是說,運行時 view1 與 template1 是在一塊兒的),把src/viewsgenerated/templates/views 輸出到同一個目錄下

可使用rootDirs指定一個roots列表,列表裏的內容會在運行時被合併,針對上面的例子,能夠這麼設置

{
  "compilerOptions": {
    "rootDirs": [
      "src/views",
      "generated/templates/views"
    ]
  }
}
複製代碼

此後只要遇到指向 rootDirs 子目錄的相對模塊引入,都會嘗試在 rootDirs 的每一項中查找,rootDirs 下的目錄不要求必須存在

跟蹤模塊解析

編譯器在解析模塊時可能訪問當前文件夾外的文件,經過--traceResolution 啓用編譯器的模塊解析追蹤,能夠查看模塊在解析過程當中發生了什麼。

使用--noResolve

--noResolve 編譯選項告訴編譯器,禁止添加任何文件(經過命令行傳入的除外)。 此時編譯器仍然會嘗試解析模塊,但再也不添加進來。

好比有這麼一個文件

import * as A from "moduleA";
import * as B from "moduleB";
複製代碼

進行這麼編譯

tsc app.ts moduleA.ts --noResolve
複製代碼

能正確引入 moduleA,而對 moduleB 的引入則會報錯找不到(假定 moduleA 和 moduleB 都正常存在)

exclude

默認狀況下,tsconfig.json 所在目錄即 TypeScript 項目目錄,不指定files或exclude的話,該目錄及其子孫目錄下的全部文件都會被添加到編譯過程當中。能夠經過 exclude 選項排除某些文件(黑名單),或者用 files 選項指定想要編譯的源文件(白名單)

編譯過程當中,被引入的模塊不管是否被 exclude 掉,都會被編譯

聲明合併

聲明合併 是指編譯器將針對同一個名字的多個獨立聲明合併爲單一聲明,合併後的聲明同時擁有原先多個聲明的特性。

ts 中的聲明會建立如下三種實體:命名空間、類型或值。

聲明類型 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};
...
/// 成員不惟一
interface Box {
    color: string
}
  
interface Box {
    color: number // 錯誤 後續屬性聲明必須屬於同一類型.
}
複製代碼

接口合併時函數成員若是同名,則會被看成同一函數的重載,後面出現的函數優先級更高

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 IDocument {
  createElement(tagName: any): Element;
}
interface IDocument {
  createElement(tagName: "div"): HTMLDivElement;
  createElement(tagName: "span"): HTMLSpanElement;
}
interface IDocument {
  createElement(tagName: string): HTMLElement;
  createElement(tagName: "canvas"): HTMLCanvasElement;
}
...
// 合併
interface IDocument {
  // 特殊簽名置頂
  createElement(tagName: "div"): HTMLDivElement;
  createElement(tagName: "span"): HTMLSpanElement;
  createElement(tagName: "canvas"): HTMLCanvasElement;
  // 下面兩條仍遵循後聲明的優先
  createElement(tagName: string): HTMLElement;
  createElement(tagName: "canvas"): HTMLCanvasElement;
}
複製代碼

除了接口合併,其餘的合併感受只會讓代碼混亂,暫時想不通爲何會有那些合併

JSX

我的感受脫離 React 討論 JSX 沒啥意義,並且React的語法不停的更新,看相關教程中介紹的語法,已通過時

裝飾器

裝飾器 是一種特殊類型的聲明,它可以被附加到類聲明方法訪問符屬性參數上。 裝飾器使用 @expression 這種形式,expression 求值後必須爲一個函數,它會在運行時被調用,被裝飾的聲明信息作爲參數傳入

要啓用實驗性的裝飾器特性,你必須在命令行或tsconfig.json裏啓用experimentalDecorators編譯器選項

tsc --target ES5 --experimentalDecorators
複製代碼
{
    "compilerOptions": {
        "target": "ES5",
        "experimentalDecorators": true
    }
}
複製代碼

裝飾器工廠

裝飾器工廠 就是一個簡單的函數,它返回一個表達式,以供裝飾器在運行時調用。

咱們能夠經過下面的方式來寫一個裝飾器工廠函數:

function color(value: string) { // 這是一個裝飾器工廠
    return function (target) { //  這是裝飾器
        // do something with "target" and "value"...
    }
}
複製代碼

類裝飾器

類裝飾器在類聲明以前被聲明,類裝飾器應用於類的構造函數,用來監視,修改或替換類定義,類裝飾器不能用在聲明文件中,也不用在任何外部上下文中

類裝飾器表達式會在運行時看成函數被調用,類的構造函數做爲其惟一的參數。

若是類裝飾器返回一個值,它會使用提供的構造函數來替換類的聲明。

/// constructor 是惟一參數
function sealed(constructor: Function) {
  Object.seal(constructor);
  Object.seal(constructor.prototype);
}

@sealed
class Greeter {
  greeting: string;
  constructor(message: string) {
    this.greeting = message;
  }
  greet() {
    return "Hello, " + this.greeting;
  }
}
let greet = new Greeter("foo");
複製代碼

@sealed 就是定義好的裝飾器,這個裝飾器能夠阻止對構造函數和原型添加屬性

方法裝飾器

方法裝飾器在一個方法的聲明以前,會被應用到方法的屬性描述上,能夠用來監視、修改或者替換方法的定義

方法裝飾器表達式會在運行時看成函數調用,傳入下列3個參數

  1. 對於靜態成員來講是類的構造函數,對於實例成員是類的原型對象。
  2. 成員的名字。
  3. 成員的屬性描述符。
class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }

    @enumerable(false)
    greet() {
        return "Hello, " + this.greeting;
    }
}
...
function enumerable(value: boolean) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        descriptor.enumerable = value;
    };
}
複製代碼

@enumerable(false) 是一個裝飾器工廠。 當裝飾器 @enumerable(false) 被調用時,它會修改屬性描述符的enumerable屬性

訪問器裝飾器

訪問器裝飾器聲明在一個訪問器的聲明以前(緊靠着訪問器聲明)。 訪問器裝飾器應用於訪問器的 屬性描述符而且能夠用來監視,修改或替換一個訪問器的定義。

TypeScript不容許同時裝飾一個成員的get和set訪問器

訪問器裝飾器表達式會在運行時看成函數被調用,傳入下列3個參數:

  1. 對於靜態成員來講是類的構造函數,對於實例成員是類的原型對象。
  2. 成員的名字。
  3. 成員的屬性描述符。

若是訪問器裝飾器返回一個值,它會被用做方法的屬性描述符

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; }
}
...
function configurable(value: boolean) {
    return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
        descriptor.configurable = value;
    };
}
複製代碼

@configurable(false) 是一個訪問器裝飾器

屬性裝飾器

屬性裝飾器聲明在一個屬性聲明以前(緊靠着屬性聲明)

屬性裝飾器表達式會在運行時看成函數被調用,傳入下列2個參數:

  1. 對於靜態成員來講是類的構造函數,對於實例成員是類的原型對象。
  2. 成員的名字。
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);
    }
}
...
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 被調用時,它讀取格式的元數據。

參數裝飾器

參數裝飾器聲明在一個參數聲明以前(緊靠着參數聲明),參數裝飾器應用於類構造函數或方法聲明

參數裝飾器表達式會在運行時看成函數被調用,傳入下列3個參數:

  1. 對於靜態成員來講是類的構造函數,對於實例成員是類的原型對象。
  2. 成員的名字。
  3. 參數在函數參數列表中的索引。

參數裝飾器的返回值會被忽略。

class Greeter {
    greeting: string;

    constructor(message: string) {
        this.greeting = message;
    }

    @validate
    greet(@required name: string) {
        return "Hello " + name + ", " + this.greeting;
    }
}
...
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 是裝飾器

裝飾器求值

類中不一樣聲明上的裝飾器按以下規定的順序應用:

  1. 參數裝飾器,而後依次是方法裝飾器,訪問符裝飾器,或屬性裝飾器應用到每一個實例成員。
  2. 參數裝飾器,而後依次是方法裝飾器,訪問符裝飾器,或屬性裝飾器應用到每一個靜態成員。
  3. 參數裝飾器應用到構造函數。
  4. 類裝飾器應用到類。

裝飾器組合

多個裝飾器能夠同時應用到一個聲明上

  • 書寫在同一行
@f @g x
複製代碼
  • 書寫在多行上
@f
@g
x
複製代碼

當多個裝飾器應用於一個聲明上,它們求值方式與複合函數類似,ts 在編譯時會進行以下步驟的操做:

  1. 由上至下對修飾器表達式求值
  2. 求值的結果會被看成函數,由下至上依次調用
function f() {
  console.log("f(): evaluated");
  return function(
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    console.log("f(): called");
  };
}

function g() {
  console.log("g(): evaluated");
  return function(
    target: any,
    propertyKey: string,
    descriptor: PropertyDescriptor
  ) {
    console.log("g(): called");
  };
}

class C {
  @f()
  @g()
  method() {
  	console.log("C method()");
  }
}
let c = new C();
c.method();
複製代碼

調用結果以下

/// 先求值(按定義順序,由上往下)
f(): evaluated
g(): evaluated
/// 依次調用(由下往上調用)
g(): called
f(): called
C method()
複製代碼
相關文章
相關標籤/搜索