TypeScript 初體驗

TypeScript👌

前言:

原由是公司大佬要求給其餘同事培訓ts,而後我本身在網上東粘粘西粘粘的,雖然不是我本身寫成的,可是也是我認真作的備課,因爲某些緣由也沒能排上用場,因此決定上傳,這裏若是大佬們看到有相同的地方,還請諒解。🌹前端

why use TypeScript?

JavaScript是一種弱類型的語言, 且JavaScript具備動態類型檢查的特徵。程序員

強類型和弱類型

強類型語言是指當一個變量一旦被指定了數據類型以後,若是不通過強制轉換,那麼他那麼它就永遠都是這個類型的了。ajax

弱類型語言是指一個變量能夠被賦值不一樣類型的數據。typescript

動態類型和靜態類型

靜態類型語言和動態類型語言得核心區別在於,靜態類型語言(statically-typed languages)會在編譯時(compile time)進行類型檢查,而動態語言(dynamically-typed)則是在運行時進行類型檢查(runtime)npm

當出現一個類型錯誤時,靜態類型檢查和動態類型檢查的差別就凸顯出來了。在靜態類型語言中,類型檢查發生在編譯階段。在動態類型語言中,只有在程序運行了一次的時候錯誤纔會被發現,也就是在運行時。編程

動態類型存在的問題

因爲JavaScript是動態類型語言,沒有編譯的環節,全部的類型檢查是在代碼運行時進行的,這就意味着咱們書寫的代碼中的某些錯誤,只能在代碼運行時在會被發現。json

好比下面這段代碼:數組

function greet(o){
  // 這句代碼,咱們嘗試去調用了obj的sayHello方法
  // 在編碼階段,咱們沒法肯定最終傳進來的參數對象到底是什麼
  // 只有在代碼實際運行階段,執行到本句代碼的時候
  // 纔會發現傳入的obj對象根本沒有sayHello方法,最終在運行階段報錯
  o.sayHello();
}

var obj = {
  name: '張學友'
}

greet(obj); // Uncaught TypeError: o.sayHello is not a function
複製代碼

再看下面這段代碼:瀏覽器

/** * 計算指定數值除以2的結果 咱們指望用戶傳入的參數爲數值類型 * @param {number} x */
function divideBy2(x) {
  return x / 2;
}

// 用戶在調用的時候若是傳入數值,則功能正常
var res = divideBy2(4);  // 2
// 可是若是用戶在調用的時候傳入一個字符串,則會致使運行時錯誤
// (不報錯,由於JavaScript解釋器會嘗試糾正錯誤,可是結果確定不是咱們預期的)
var res1 = divideBy2('Hello World'); // NaN
複製代碼

固然,動態類型帶來的並不僅是問題,它的靈活程度和編碼成本相較於靜態類型的語言來說是顯而易見的。好比在Java和C#被類型限制的生活不能自理的同窗,在JavaScript中你幾乎能夠放飛自我,隨心所欲(Just a joke)。bash

靜態類型帶來的好處

你能夠儘早發現bug和錯誤

靜態類型檢查容許咱們在程序沒有運行以前就能夠肯定咱們所設定的肯定性是不是對的。一旦有違反這些既定規則的行爲,它能在運行以前就發現,而不是在運行時。

由於類型檢查器會在你編碼的時候就告訴你錯誤,因此這也就比你把代碼交付到客戶手中才發現一些錯誤要更方面(或者說付出更少的開發與維護成本)。

提升代碼的可讀性

在代碼中加入類型系統,能夠清晰的告訴用戶功能所須要的數據是什麼類型的,函數的返回值是什麼類型的,提高代碼的可讀性。

減小了複雜的錯誤處理邏輯

假設咱們須要提供一個函數用來計算數組中全部數字的和

// 最基本的代碼以下:
let  sum = arr => {
  let result = 0;
  arr.forEach(v => {
    result += v;
  })
  return result;
}

// 但是上面的代碼對於可能出現的異常沒有作任何的處理
// 爲了保證函數可以正常的運行,咱們須要確保用戶傳入的參數爲有效的數字數組
// 那麼就須要這麼作
let  sum = arr => {
  if(!arr){
    throw new Error("Please give me arguments");
  }

  if(!Array.isArray(arr)){
    throw new Error("I need Array, what you've passed to me?");
  }

  if(!arr.every(v => typeof v == 'number')){
    throw new Error("你傳進來的數組裏有奇怪的東西,我要的是數字!")
  }

  let result = 0;
  arr.forEach(v => {
    result += v;
  })
  return result;
}
複製代碼

如此咱們便發現,若是沒有類型系統,要處理相似的問題,代碼顯得很是繁瑣。

當有了類型系統以後,這樣代碼就不須要再寫了,在咱們學習完flowtypescript以後咱們回過頭來再看這個例子。

促進更可靠的重構

假設要進行代碼重構,咱們須要將函數的某個參數進行修改,那麼在以前修改的時候咱們可能須要猶豫,由於指不定項目中某個地方調用沒有進行修改,那麼運行的時候會產生奇怪的問題。

而有了靜態類型檢測以後,類型檢測會自動告訴咱們修改後的代碼哪裏存在問題,咱們只須要按照提示修復便可。

加強IDE的功能

靜態類型會加強IDE的功能,提高開發效率。

靜態類型存在的問題

  1. 會增長代碼量
  2. 須要花時間掌握類型
  3. 可能會下降開發效率

如何在JavaScript開發中使用靜態類型

  1. Flow: FaceBook的開源技術
  2. TypeScript: 微軟公司開發的語言

ts是什麼?

TypeScript是由微軟公司開發的一個開源JavaScript的超集,主要提供了類型系統和對ES6的支持,他能夠編譯成純 JavaScript. 任何現有的 JavaScript 都是合法的 TypeScript 程序。

TypeScript從出現至今已經成爲了前端領域中不可忽視的技術,各大流行框架都已經支持使用 TypeScript 做爲開發語言。

  • TypeScript是微軟公司開發的一款開源的JavaScript超集語言!
  • JavaScript超集: 當前任何JavaScript都是合法的TypeScript代碼!
  • TypeScript主要爲JavaScript提供了類型系統和ES6語法的支持!
  • Flow是一個類型檢查工具,TypeScript是一種開發語言!
  • TypeScript有本身的編譯工具,咱們寫好的TypeScript代碼最終會經過編譯器編譯成JavaScript代碼進行運行!

安裝 TypeScript

TypeScript 最終要運行起來,咱們須要將 TypeScript 代碼轉換成對應的 JavaScript 代碼,那麼 TypeScript 的命令行工具就能夠幫咱們完成這件事情。

TypeScript 的命令行工具安裝方法以下:

npm install -g typescript
複製代碼

以上命令會在全局環境下安裝 tsc 命令,安裝完成以後,咱們就能夠在任何地方執行 tsc 命令了。

編譯一個 TypeScript 文件很簡單:

tsc hello.ts
複製代碼

咱們約定使用 TypeScript 編寫的文件以 .ts 爲後綴

ts配置文件的說明

  1. 建立配置文件
tsc --init
複製代碼
  1. 設置配置項

    • target: 指的就是將ts代碼要轉換成哪一個版本的js代碼 es5 es3
    • module: 指的就是將ts代碼轉換成js代碼以後,使用的模塊化的標準是什麼
    • outDir: 指的就是將ts代碼轉換成js代碼以後,js代碼存放的文件夾路徑
    • rootDir: 指的就是要將哪一個目錄中的ts代碼進型轉換,ts代碼的存放路徑
    • strict: 是否要將ts代碼轉換爲嚴格模式的js代碼!
  2. 使用配置文件

    tsc -p ./tsconfig.json
    複製代碼

TypeScript 初體驗

接下來咱們書寫一段代碼,你們不須要糾結詳細的技術點,只須要看 TypeScript 給咱們帶來的功能體驗便可。

function greeter(msg: string) {
  return "Hello, " + msg;
}

let str = "World";
console.log(greeter(str)); // Hello,World
複製代碼

上面這段代碼就是一段 TypeScript 代碼,咱們在這段代碼中規定了函數greeter傳入的參數必須類型是string。咱們將這段代碼保存爲greeter.ts文件

接下來咱們在命令行中執行

tsc greeter.ts
複製代碼

這個命令會將咱們寫好的 ts 文件轉換成相應的 JavaScript 代碼文件

function greeter(msg) {
  return "Hello, " + msg;
}
var str = "World";
console.log(greeter(msg));
複製代碼

上述例子中,咱們用 : 指定 greeter參數類型爲 string。可是編譯爲 js 以後,並無什麼檢查的代碼被插入進來。

TypeScript 只會進行靜態檢查,若是發現有錯誤,編譯的時候就會報錯

下面嘗試把這段代碼編譯一下:

function greeter(msg: string) {
  return "Hello, " + msg;
}

let str = [0, 1, 2];
console.log(greeter(str));
複製代碼

編輯器中會提示錯誤,編譯的時候也會出錯:

index.ts(6,22): error TS2345: Argument of type 'number[]' is not assignable to parameter of type 'string'.
複製代碼

可是仍是生成了 js 文件:

function greeter(msg) {
  return "Hello, " + msg;
}

let str = [0, 1, 2];
console.log(greeter(str));
複製代碼

TypeScript 編譯的時候即便報錯了,仍是會生成編譯結果,咱們仍然可使用這個編譯以後的文件。

TypeScript 數據類型

介紹

爲了讓程序有價值,咱們須要可以處理最簡單的數據單元:數字,字符串,結構體,布爾值等。 TypeScript 支持與 JavaScript 幾乎相同的數據類型,此外還提供了實用的枚舉類型方便咱們使用。

布爾值

最基本的數據類型就是簡單的 true/false 值,在 JavaScript 和 TypeScript 裏叫作boolean(其它語言中也同樣)。

let isDone: 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 = "smith";
複製代碼

你還可使用模版字符串,它能夠定義多行文本和內嵌表達式。 這種字符串是被反引號包圍(```),而且以${ expr }這種形式嵌入表達式

let name: string = `Gene`;
let age: number = 37;
let sentence: string = `Hello, my name is ${name}. I'll be ${age + 1} years old next month.`;
複製代碼

這與下面定義sentence的方式效果相同:

let sentence: string =
  "Hello, my name is " +
  name +
  ".\n\n" +
  "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

元組類型容許表示一個已知元素數量和類型的數組,各元素的類型沒必要相同。 好比,你能夠定義一對值分別爲stringnumber類型的元組。

// 聲明一個元祖類型
let x: [string, number];
// Initialize it
x = ["hello", 10]; // OK
// Initialize it incorrectly
x = [10, "hello"]; // Error
複製代碼

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

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

當訪問一個越界的元素,會使用聯合類型替代:

x[3] = "world"; // error

console.log(x[5].toString()); // error

x[6] = true; // Error, 布爾不是(string | number)類型
複製代碼

聯合類型是高級主題,咱們會在之後的章節裏討論它。

枚舉

enum類型是對 JavaScript 標準數據類型的一個補充。 像 C#等其它語言同樣,使用枚舉類型能夠爲一組數值賦予友好的名字。

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

默認狀況下,從0開始爲元素編號。 你也能夠手動的指定成員的數值。 例如,咱們將上面的例子改爲從1開始編號:

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

或者,所有都採用手動賦值:

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

枚舉類型提供的一個便利是你能夠由枚舉的值獲得它的名字。 例如,咱們知道數值爲 2,可是不肯定它映射到 Color 裏的哪一個名字,咱們能夠查找相應的名字:

enum Color {
  Red = 1,
  Green,
  Blue
}
let colorName: string = Color[2];

alert(colorName); // 顯示'Green'由於上面代碼裏它的值是2
複製代碼

任意值 Any

有時候,咱們會想要爲那些在編程階段還不清楚類型的變量指定一個類型。 這些值可能來自於動態的內容,好比來自用戶輸入或第三方代碼庫。 這種狀況下,咱們不但願類型檢查器對這些值進行檢查而是直接讓它們經過編譯階段的檢查。 那麼咱們可使用any類型來標記這些變量:

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

在對現有代碼進行改寫的時候,any類型是十分有用的,它容許你在編譯時可選擇地包含或移除類型檢查。 你可能認爲Object有類似的做用,就像它在其它語言中那樣。 可是Object類型的變量只是容許你給它賦任意值 - 可是卻不可以在它上面調用任意的方法,即使它真的有這些方法:

let notSure: any = 4;
notSure.ifItExists(); // okay, ifItExists might exist at runtime
notSure.toFixed(); // okay, toFixed exists (but the compiler doesn't check)

let prettySure: Object = 4;
prettySure.toFixed(); // Error: Property 'toFixed' doesn't exist on type 'Object'.
複製代碼

當你只知道一部分數據的類型時,any類型也是有用的。 好比,你有一個數組,它包含了不一樣的類型的數據:

let list: any[] = [1, true, "free"];

list[1] = 100;
複製代碼

Emmm...就是什麼類型都行,當你沒法確認在處理什麼類型時能夠用這個。

但要慎重使用,用多了就失去使用Ts的意義。

主要應用場景有:

  1. 接入第三方庫
  2. Ts菜逼前期都用:joy:

空值 Void

某種程度上來講,void類型像是與any類型相反,它表示沒有任何類型。 當一個函數沒有返回值時,你一般會見到其返回值類型是void

function warnUser(): void {
  alert("This is my warning message");
  // 注意 這個時候 咱們不能使用return
}
複製代碼

Typescript中,你必須在函數中定義返回類型。像這樣:

若沒有返回值,則會報錯:

咱們能夠將其返回值定義爲void:

此時將沒法 return

聲明一個void類型的變量沒有什麼大用,由於你只能爲它賦予undefinednull

let unusable: void = undefined;
複製代碼

Null 和 Undefined

TypeScript 裏,undefinednull二者各自有本身的類型分別叫作undefinednull。 和void類似,它們的自己的類型用處不是很大:

// Not much else we can assign to these variables!
let u: undefined = undefined;
let n: null = null;
複製代碼

默認狀況下nullundefined是全部類型的子類型。 就是說你能夠把nullundefined賦值給number類型的變量。

然而,當你指定了--strictNullChecks標記,nullundefined只能賦值給void和它們各自。 這能避免不少常見的問題。 也許在某處你想傳入一個stringnullundefined,你可使用聯合類型string | null | undefined。 再次說明,稍後咱們會介紹聯合類型。

注意:咱們鼓勵儘量地使用--strictNullChecks,但在本手冊裏咱們假設這個標記是關閉的。

Never

never類型表示的是那些永不存在的值的類型。 例如,never類型是那些老是會拋出異常或根本就不會有返回值的函數表達式或箭頭函數表達式的返回值類型; 變量也多是never類型,當它們被永不爲真的類型保護所約束時。

never類型是任何類型的子類型,也能夠賦值給任何類型;然而,沒有類型是never的子類型或能夠賦值給never類型(除了never自己以外)。 即便any也不能夠賦值給never

下面是一些返回never類型的函數:

// 返回never的函數必須存在沒法達到的終點
function error(message: string): never {
  throw new Error(message);
}

// 推斷的返回值類型爲never
function fail() {
  return error("Something failed");
}

// 返回never的函數必須存在沒法達到的終點
function infiniteLoop(): never {
  while (true) {}
}
複製代碼

Object

object表示非原始類型,也就是除numberstringbooleansymbolnullundefined以外的類型。

使用object類型,就能夠更好的表示像Object.create這樣的 API。例如:

declare function create(o: object | null): void;

create({ prop: 0 }); // OK
create(null); // OK

create(42); // Error
create("string"); // Error
create(false); // Error
create(undefined); // Error
複製代碼

類型斷言

有時候你會遇到這樣的狀況,你會比 TypeScript 更瞭解某個值的詳細信息。 一般這會發生在你清楚地知道一個實體具備比它現有類型更確切的類型。

經過類型斷言這種方式能夠告訴編譯器,「相信我,我知道本身在幹什麼」。 類型斷言比如其它語言裏的類型轉換,可是不進行特殊的數據檢查和解構。 它沒有運行時的影響,只是在編譯階段起做用。 TypeScript 會假設你,程序員,已經進行了必須的檢查。

簡略的定義是:能夠用來手動指定一個值的類型。

類型斷言有兩種形式。 其一是「尖括號」語法:

let someValue: any = "this is a string";

let strLength: number = (<string>someValue).length;
複製代碼

另外一個爲as語法:

let someValue: any = "this is a string";

let strLength: number = (someValue as string).length;
複製代碼

兩種形式是等價的。 至於使用哪一個大多數狀況下是憑我的喜愛;然而,當你在 TypeScript 裏使用 JSX 時,只有as語法斷言是被容許的。

關於let

你可能已經注意到了,咱們使用let關鍵字來代替你們所熟悉的 JavaScript 關鍵字varlet關鍵字是 JavaScript 的一個新概念,TypeScript 實現了它。 咱們會在之後詳細介紹它,不少常見的問題均可以經過使用let來解決,因此儘量地使用let來代替var吧。

介紹

傳統的 JavaScript 程序使用函數和基於原型的繼承來建立可重用的組件,但對於熟悉使用面向對象方式的程序員來說就有些棘手,由於他們用的是基於類的繼承而且對象是由類構建出來的。 從 ECMAScript 2015,也就是 ECMAScript 6 開始,JavaScript 程序員將可以使用基於類的面向對象的方式。 使用 TypeScript,咱們容許開發者如今就使用這些特性,而且編譯後的 JavaScript 能夠在全部主流瀏覽器和平臺上運行,而不須要等到下個 JavaScript 版本。

下面看一個使用類的例子:

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類型的新對象,並執行構造函數初始化它。

簡單示例:

class person {
    // 和ES6不一樣的是, ts 中屬性必須聲明, 須要指定類型
    name: string;
    // 聲明好屬性以後, 屬性必須賦值一個默認值或者在構造函數中進行初始化
    age: number;
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }
    sayHello(msg: string): void {
        console.log(msg);
    }
}

let xm = new person('小明', 18);
xm.sayHello('你們好');
複製代碼

繼承

在 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是一個派生類,它派生自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();
// Slithering...
// Sammy the Python moved 5m.
tom.move(34);
// Galloping...
// Tommy the Palomino moved 34m.
複製代碼

這個例子展現了一些上面沒有提到的特性。 這一次,咱們使用extends關鍵字建立了Animal的兩個子類:HorseSnake

與前一個例子的不一樣點是,派生類包含了一個構造函數,它必須調用super(),它會執行基類的構造函數。 並且,在構造函數裏訪問this的屬性以前,咱們必定要調用super()。 這個是 TypeScript 強制執行的一條重要規則。

這個例子演示瞭如何在子類裏能夠重寫父類的方法。 Snake類和Horse類都建立了move方法,它們重寫了從Animal繼承來的move方法,使得move方法根據不一樣的類而具備不一樣的功能。 注意,即便tom被聲明爲Animal類型,但由於它的值是Horse,調用tom.move(34)時,它會調用Horse裏重寫的方法

簡單示例:

class Animal {
    age: number;
    constructor(age: number) {
        this.age = age;
    }
    eat() {
        console.log('吃個大雞腿');
    }
}

class Dog extends Animal{
    type: string;
    constructor(type: string, age: number) {
        // 要使用super
        super(age);
        this.type = type;
    }
    // 若是子類中出現了和父類相同名字的方法,則會進行覆蓋
    // 也就是調用的時候, 調用的是子類中的方法了!
    eat () {
        console.log('我是狗對象中的eat方法');
    }
}
let dog = new Dog('哈士奇', 2);
dog.eat();

複製代碼

公共,私有與受保護的修飾符

指的就是能夠在類的成員前經過添加關鍵字來設置當前成員的訪問權限

  • public: 公開的,默認 全部人均可以進行訪問
  • private: 私有的, 只能在當前類中進行訪問
  • protected: 受保護的,這能在當前類或者子類中進行訪問

默認爲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 使用的是結構性類型系統。 當咱們比較兩種不一樣的類型時,並不在意它們從何處而來,若是全部成員的類型都是兼容的,咱們就認爲它們的類型是兼容的。

然而,當咱們比較帶有privateprotected成員的類型的時候,狀況就不一樣了。 若是其中一個類型裏包含一個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 不兼容.
複製代碼

這個例子中有AnimalRhino兩個類,RhinoAnimal類的子類。 還有一個Employee類,其類型看上去與Animal是相同的。 咱們建立了幾個這些類的實例,並相互賦值來看看會發生什麼。 由於AnimalRhino共享了來自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' 的構造函數是被保護的.
複製代碼

簡單示例:

// ts 中類成員的訪問修飾符
// ts 中的修飾符指的就是能夠在類的成員前經過添加關鍵字來設置當前成員的訪問權限

// public: 公開的, 默認 全部人均可以進行訪問
// private: 私有的, 只有在當前類中進行訪問
// protected: 受保護的 只能在當前類或者子類中進行訪問


enum Colors {
    red,
    yellow,
    blue
}

class Car {
    public color: Colors;
    constructor() {
        this.color = Colors.red
        this.run();
        this.loadPeople();
    }
    private run () {

    }
    protected loadPeople () {

    }
}

let aoDi = new Car();
// aoDi.color;
aoDi.color;
// aoDi.run();
// aoDi.loadPeople();

class Byd extends Car {
    sayHi() {
        this.loadPeople();
        // this.run(); // error
        console.log(this.color);
    }
}
let bw = new Byd();
bw.color;
bw.sayHi()
// benchi.loadPeople(); error
// benchi.run(); // error
複製代碼

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和一個構造函數參數theNamePerson類裏,而且馬上將theName的值賦給name。 這種狀況常常會遇到。參數屬性能夠方便地讓咱們在一個地方定義並初始化一個成員。 下面的例子是對以前Animal類的修改版,使用了參數屬性:

class Animal {
  constructor(private name: string) {}
  move(distanceInMeters: number) {
    console.log(`${this.name} moved ${distanceInMeters}m.`);
  }
}
複製代碼

注意看咱們是如何捨棄了theName,僅在構造函數裏使用private name: string參數來建立和初始化name成員。 咱們把聲明和賦值合併至一處。

參數屬性經過給構造函數參數添加一個訪問限定符來聲明。 使用private限定一個參數屬性會聲明並初始化一個私有成員;對於publicprotected來講也是同樣。

簡單示例:

class Cat {
    readonly name: string;
    // type: string
    constructor(public type: string) {
        this.name = '加菲'
        this.type = type
    }
}
let cat = new Cat('橘貓');
// cat.name = 'qqq';

複製代碼

存取器

TypeScript 支持經過 getters/setters 來截取對對象成員的訪問。 它能幫助你有效的控制對對象成員的訪問。

下面來看如何把一個簡單的類改寫成使用getset。 首先,咱們從一個沒有使用存取器的例子開始。

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

簡單示例

// class People {
// name: string = ''
// }
// let p1 = new People();
// p1.name = 'S是的是的沙發地方的';

 
class People {
    private _name:string = "";
    get name(): string {
        return this._name;
    }
    set name(value: string) {
        if (value.length < 2 || value.length > 5) {
            throw new Error('名字不合法,請從新輸入');
        }
        this._name = value;
    }
}

let p1 = new People();
p1.name = '是的是的是打算';
console.log(p1.name);
複製代碼

接口

介紹

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

接口初探

下面經過一個簡單示例來觀察接口是如何工做的:

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

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

類型檢查器會查看printLabel的調用。 printLabel有一個參數,並要求這個對象參數有一個名爲label類型爲string的屬性。 須要注意的是,咱們傳入的對象參數實際上會包含不少屬性,可是編譯器只會檢查那些必需的屬性是否存在,而且其類型是否匹配。 然而,有些時候 TypeScript 卻並不會這麼寬鬆,咱們下面會稍作講解。

下面咱們重寫上面的例子,此次使用接口來描述:必須包含一個label屬性且類型爲string

interface LabelledValue {
  label: string;
}

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

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

LabelledValue接口就比如一個名字,用來描述上面例子裏的要求。 它表明了有一個label屬性且類型爲string的對象。 須要注意的是,咱們在這裏並不能像在其它語言裏同樣,說傳給printLabel的對象實現了這個接口。咱們只會去關注值的外形。 只要傳入的對象知足上面提到的必要條件,那麼它就是被容許的。

還有一點值得提的是,類型檢查器不會去檢查屬性的順序,只要相應的屬性存在而且類型也是對的就能夠。

簡單示例:

// 接口
// 接口咱們能夠理解爲一個約定, 一個規範


// 接口使用 interface 進行聲明
interface AjaxOptions {
    url: string,
    type: string,
    data: object,
    success(data: object): void
}
// options參數中 須要包含 url, type, data, success 
function ajax(options: AjaxOptions) {

}
ajax({
    url:'https://www.baidu.com',
    type: 'post',
    data:{},
    success(data) {

    }
})
複製代碼

可選屬性

接口裏的屬性不全都是必需的。 有些是隻在某些條件下存在,或者根本不存在。 可選屬性在應用「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.clor) {
    // 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。 賦值後,xy不再能被改變了。

let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error!
複製代碼

額外的屬性檢查

咱們在第一個例子裏使用了接口,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帶有上面定義的類型的colorwidth屬性,而且還會帶有任意數量的其它屬性,那麼咱們能夠這樣定義它:

interface SquareConfig {
  color?: string;
  width?: number;
  [propName: string]: any;
}
複製代碼

咱們稍後會講到索引簽名,但在這咱們要表示的是SquareConfig能夠有任意數量的屬性,而且只要它們不是colorwidth,那麼就無所謂它們的類型是什麼。

還有最後一種跳過這些檢查的方式,這可能會讓你感到驚訝,它就是將這個對象賦值給一個另外一個變量: 由於squareOptions不會通過額外屬性檢查,因此編譯器不會報錯。

let squareOptions = { colour: "red", width: 100 };
let mySquare = createSquare(squareOptions);
複製代碼

要留意,在像上面同樣的簡單代碼裏,你可能不該該去繞開這些檢查。 對於包含方法和內部狀態的複雜對象字面量來說,你可能須要使用這些技巧,可是大部額外屬性檢查錯誤是真正的 bug。 就是說你遇到了額外類型檢查出的錯誤,好比「option bags」,你應該去審查一下你的類型聲明。 在這裏,若是支持傳入colorcolour屬性到createSquare,你應該修改SquareConfig定義來體現出這一點。

簡單示例:

// 只讀屬性
 interface Poin {
    readonly x: number, // 只讀屬性
    y: number,
    [propName: string]: any // 額外的類型檢查
 }
 let pos: Poin = {
     x: 10,
     y: 20,
     z: 40
 }

// pos.x = 40; // error

複製代碼

函數類型

接口可以描述 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類型變量。 函數的返回值類型是經過其返回值推斷出來的(此例是falsetrue)。 若是讓這個函數返回數字或字符串,類型檢查器會警告咱們函數的返回值類型與SearchFunc接口中的定義不匹配。

let mySearch: SearchFunc;
mySearch = function(src, sub) {
  let result = src.search(sub);
  return result > -1;
};
複製代碼

簡單示例:

interface SumInterface{
    (a: number, b: number): number
}
let sum: SumInterface = function (a: number, b: number) {
    return a + b ;
}
複製代碼

類類型

實現接口

與 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 PersonInterface {
    name: string,
    age: number,
    eat(): void
}

// 使用 implements 關鍵字
class XiaoMing implements PersonInterface {
    name: string = '小明';
    age: number = 19;
    eat() {

    }
}
複製代碼

繼承接口

和類同樣,接口也能夠相互繼承。 這讓咱們可以從一個接口裏複製成員到另外一個接口裏,能夠更靈活地將接口分割到可重用的模塊裏。

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

簡單示例:

// 接口繼承接口
interface TwoPoint {
    x: number,
    y: number
}
interface ThreePoint {
    z: number
}
interface FourPoint extends ThreePoint, TwoPoint {
    time: Date
}

let poi2: FourPoint = {
    x: 100,
    y: 200,
    z: 300,
    time: new Date()
}
複製代碼

接口繼承類

當接口繼承了一個類類型時,它會繼承類的成員但不包括其實現。 就好像接口聲明瞭全部類中存在的成員,但並無提供具體實現同樣。 接口一樣會繼承到類的 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 {
  select() {}
}

// 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方法。 ButtonTextBox類是SelectableControl的子類(由於它們都繼承自Control並有select方法),但ImageLocation類並非這樣的。

// 接口繼承類
class Bird {
    type: string = '畫眉鳥';
    fly():void {

    }
}
interface Fly extends Bird {

}
let flyingBrid: Fly = {
    type: '啄木鳥',
    fly(): void {

    }
}


// 接口繼承類

class Bird {
    type: string;
    eat():void {

    }
}

interface Fly extends Bird {
    fly():void
}

let flyBird: Fly = {
    type: '黃鸝',
    eat(): void{

    },
    fly(): void {

    }
}
複製代碼

泛型

泛型:Generics

軟件工程的一個主要部分就是構建組件,構建的組件不只須要具備明確的定義和統一的接口,同時也須要組件可複用。支持現有的數據類型和未來添加的數據類型的組件爲大型軟件系統的開發過程提供很好的靈活性。

C#Java中,可使用"泛型"來建立可複用的組件,而且組件可支持多種數據類型。這樣即可以讓用戶根據本身的數據類型來使用組件。

1. 泛型方法

在TypeScript裏,聲明泛型方法有如下兩種方式:

function gen_func1<T>(arg: T): T {
    return arg;
}
// 或者
let gen_func2: <T>(arg: T) => T = function (arg) {
    return arg;
}
// gen_func2:<T>(arg: T) => T = arg => arg
複製代碼

調用方式也有兩種:

gen_func1<string>('Hello world');
gen_func2('Hello world'); 
// 第二種調用方式可省略類型參數,由於編譯器會根據傳入參數來自動識別對應的類型。
複製代碼

2. 泛型與Any

Ts 的特殊類型 Any 在具體使用時,能夠代替任意類型,咋一看二者好像沒啥區別,其實否則:

// 方法一:帶有any參數的方法
function any_func(arg: any): any {
    console.log(arg.length);
		return arg;
}

// 方法二:
function loggingIdentity<T>(arg: T): T {
    console.log(arg.length);  // Error: T doesn't have .length return arg; } // 方法二:Array泛型方法 function array_func<T>(arg: Array<T>): Array<T> { console.log(arg.length); return arg; } 複製代碼
  • 方法二,打印了arg參數的length屬性。由於any能夠代替任意類型,因此該方法在傳入參數不是數組或者帶有length屬性對象時,會拋出異常。
  • 方法三, 定義了參數類型是Array的泛型類型,確定會有length屬性,因此不會拋出異常。

3. 泛型類型

泛型接口:

interface Generics_interface<T> {
    (arg: T): T;
}
 
function func_demo<T>(arg: T): T {
    return arg;
}

let func1: Generics_interface<number> = func_demo;
func1(123);     // 正確類型的實際參數
func1('123');   // 錯誤類型的實際參數
複製代碼

結束語

你沒法決定明天是晴仍是雨,愛你的人是否還能留在身邊,你此刻的堅持能換來什麼,但你能決定今天有沒有準備好雨傘,有沒有好好愛人,以及是否足夠努力。永遠不要只看見前方路途遙遠而忘了本身堅持多久才走到這裏,今天盡力作的雖然辛苦,但將來發生的都是禮物。 以此共勉 😊

相關文章
相關標籤/搜索