1. 程式人生 > 其它 >TypeScript基礎入門

TypeScript基礎入門

TypeScript學習筆記

TypeScript歷史

JavaScript一門優秀的語言

我始終相信:任何新技術的出現都是為了解決原有技術的某個痛點。

JavaScript是一門優秀的程式語言嗎?

  • 每個人可能觀點並不完全一致,但是從很多角度來看,JavaScript是一門非常優秀的程式語言;
  • 而且,可以說在很長一段時間內這個語言不會被代替,並且會在更多的領域被大家廣泛使用;

著名的Atwood定律:

  • Stack Overflow的創立者之一的 Jeff Atwood 在2007年提出了著名的 Atwood定律。
  • any application that can be written in JavaScript, will eventually be written in JavaScript.
  • 任何可以使用JavaScript來實現的應用都最終都會使用JavaScript實現。

其實我們已經看到了,這句話正在一步步被應驗:

  • Web端的開發我們一直都是使用JavaScript;
  • 移動端開發可以藉助於ReactNative、Weex、Uniapp等框架實現跨平臺開發;
  • 小程式端的開發也是離不開JavaScript;
  • 桌面端應用程式我們可以藉助於Electron來開發;
  • 伺服器端開發可以藉助於Node環境使用JavaScript來開發。

JavaScript的痛點

並且隨著近幾年前端領域的快速發展,讓JavaScript迅速被普及和受廣大開發者的喜愛,藉助於JavaScript本身的強大,也讓使用JavaScript開發的人員越來越多。

優秀的JavaScript沒有缺點嗎?

  • 其實上由於各種歷史因素,JavaScript語言本身存在很多的缺點;
  • 比如ES5以及之前的使用的var關鍵字有作用域的問題;
  • 比如最初JavaScript設計的陣列型別並不是連續的記憶體空間;
  • 比如直到今天JavaScript也沒有加入型別檢測這一機制;

JavaScript正在慢慢變好

  • 不可否認的是,JavaScript正在慢慢變得越來越好,無論是從底層設計還是應用層面。
  • ES6、7、8等的推出,每次都會讓這門語言更加現代、更加安全、更加方便。
  • 但是直到今天,JavaScript在型別檢測上依然是毫無進展(為什麼型別檢測如此重要,我後面會聊到)。

型別帶來的問題

首先你需要知道,程式設計開發中我們有一個共識:錯誤出現的越早越好

  • 能在寫程式碼的時候發現錯誤,就不要在程式碼編譯時再發現(IDE的優勢就是在程式碼編寫過程中幫助我們發現錯誤)。
  • 能在程式碼編譯期間發現錯誤,就不要在程式碼執行期間再發現(型別檢測就可以很好的幫助我們做到這一點)。
  • 能在開發階段發現錯誤,就不要在測試期間發現錯誤,能在測試期間發現錯誤,就不要在上線後發現錯誤。

現在我們想探究的就是如何在 程式碼編譯期間 發現程式碼的錯誤:

  • JavaScript可以做到嗎?不可以,我們來看下面這段經常可能出現的程式碼問題。

型別錯誤

這是我們一個非常常見的錯誤:

  • 這個錯誤很大的原因就是因為JavaScript沒有對我們傳入的引數進行任何的限制,只能等到執行期間才發現這個錯誤;
  • 並且當這個錯誤產生時,會影響後續程式碼的繼續執行,也就是整個專案都因為一個小小的錯誤而深入崩潰;

當然,你可能會想:我怎麼可能犯這樣低階的錯誤呢?

  • 當我們寫像我們上面這樣的簡單的demo時,這樣的錯誤很容易避免,並且當出現錯誤時,也很容易檢查出來;
  • 但是當我們開發一個大型專案時呢?你能保證自己一定不會出現這樣的問題嗎?而且如果我們是呼叫別人的類庫,又如何知道讓我們傳入的到底是什麼樣的引數呢?

但是,如果我們可以給JavaScript加上很多限制,在開發中就可以很好的避免這樣的問題了:

  • 比如我們的getLength函式中str是一個必傳的型別,沒有呼叫者沒有傳編譯期間就會報錯;
  • 比如我們要求它的必須是一個String型別,傳入其他型別就直接報錯;
  • 那麼就可以知道很多的錯誤問題在編譯期間就被發現,而不是等到執行時再去發現和修改;

型別思維的缺失

我們已經簡單體會到沒有型別檢查帶來的一些問題,JavaScript因為從設計之初就沒有考慮型別的約束問題,所以造成了前端開發人員關於型別思維的缺失:

  • 前端開發人員通常不關心變數或者引數是什麼型別的,如果在必須確定型別時,我們往往需要使用各種判斷驗證;
  • 從其他方向轉到前端的人員,也會因為沒有型別約束,而總是擔心自己的程式碼不安全,不夠健壯;

所以我們經常會說JavaScript不適合開發大型專案,因為當專案一旦龐大起來,這種寬鬆的型別約束會帶來非常多的安全隱患,多人員開發它們之間也沒有良好的型別契約

  • 比如當我們去實現一個核心類庫時,如果沒有型別約束,那麼需要對別人傳入的引數進行各種驗證來保證我們程式碼的健壯性;
  • 比如我們去呼叫別人的函式,對方沒有對函式進行任何的註釋,我們只能去看裡面的邏輯來理解這個函式需要傳入什麼引數,返回值是什麼型別

JavaScript新增型別約束

為了彌補JavaScript型別約束上的缺陷,增加型別約束,很多公司推出了自己的方案:

  • 2014年,Facebook推出了flow來對JavaScript進行型別檢查;
  • 同年,Microsoft微軟也推出了TypeScript1.0版本;
  • 他們都致力於為JavaScript提供型別檢查

而現在,無疑TypeScript已經完全勝出:

  • Vue2.x的時候採用的就是flow來做型別檢查;
  • Vue3.x已經全線轉向TypeScript,98.3%使用TypeScript進行了重構;
  • 而Angular在很早期就使用TypeScript進行了專案重構並且需要使用TypeScript來進行開發
  • 而甚至Facebook公司一些自己的產品也在使用TypeScript;

學習TypeScript不僅僅可以為我們的程式碼增加型別約束,而且可以培養我們前端程式設計師具備型別思維。

認識TypeScript

雖然我們已經知道TypeScript是幹什麼的了,也知道它解決了什麼樣的問題,但是我們還是需要全面的來認識一下TypeScript到底是什麼?

我們來看一下TypeScript在GitHub和官方上對自己的定義:

  • GitHub說法:TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
  • TypeScript官網:TypeScript is a typed superset of JavaScript that compiles to plain JavaScript.
  • 翻譯一下:TypeScript是擁有型別的JavaScript超集,它可以編譯成普通、乾淨、完整的JavaScript程式碼

怎麼理解上面的話呢?

  • 我們可以將TypeScript理解成加強版的JavaScript
  • JavaScript所擁有的特性,TypeScript全部都是支援的,並且它緊隨ECMAScript的標準,所以ES6、ES7、ES8等新語法標準,它都是支援的;
  • 並且在語言層面上,不僅僅增加了型別約束,而且包括一些語法的擴充套件,比如列舉型別(Enum)、元組型別(Tuple)等;
  • TypeScript在實現新特性的同時,總是保持和ES標準的同步甚至是領先;
  • 並且TypeScript最終會被編譯成JavaScript程式碼,所以你並不需要擔心它的相容性問題,在編譯時也不需要藉助於Babel這樣的工具;
  • 所以,我們可以把TypeScript理解成更加強大的JavaScript,不僅讓JavaScript更加安全,而且給它帶來了諸多好用的好用特性;

TypeScript的特點

官方對TypeScript有幾段特點的描述,我覺得非常到位(雖然有些官方,瞭解一下),我們一起來分享一下:

  • 始於JavaScript,歸於JavaScript
    • TypeScript從今天數以百萬計的JavaScript開發者所熟悉的語法和語義開始。使用現有的JavaScript程式碼,包括流行的JavaScript庫,並從JavaScript程式碼中呼叫TypeScript程式碼;
    • TypeScript可以編譯出純淨、 簡潔的JavaScript程式碼,並且可以執行在任何瀏覽器上、Node.js環境中和任何支援ECMAScript 3(或更高版本)的JavaScript引擎中;
  • TypeScript是一個強大的工具,用於構建大型專案
    • 型別允許JavaScript開發者在開發JavaScript應用程式時使用高效的開發工具和常用操作比如靜態檢查和程式碼重構;
    • 型別是可選的,型別推斷讓一些型別的註釋使你的程式碼的靜態驗證有很大的不同。型別讓你定義軟體元件之間的介面和洞察現有JavaScript庫的行為;
  • 擁有先進的 JavaScript
    • TypeScript提供最新的和不斷髮展的JavaScript特性,包括那些來自2015年的ECMAScript和未來的提案中的特性,比如非同步功能和Decorators(裝飾器),以幫助建立健壯的元件;
    • 這些特性為高可信應用程式開發時是可用的,但是會被編譯成簡潔的ECMAScript3(或更新版本)的JavaScript;

眾多專案採用TypeScript

正是因為有這些特性,TypeScript目前已經在很多地方被應用:

  • Angular原始碼在很早就使用TypeScript來進行了重寫,並且開發Angular也需要掌握TypeScript;
  • Vue3原始碼也採用了TypeScript進行重寫,在前面閱讀原始碼時我們看到大量TypeScript的語法;
  • 包括目前已經變成最流行的編輯器VSCode也是使用TypeScript來完成的;
  • 包括在React中已經使用的ant-design的UI庫,也大量使用TypeScript來編寫;
  • 目前公司非常流行Vue3+TypeScript、React+TypeScript的開發模式;
  • 包括小程式開發,也是支援TypeScript的;

前端學不動系列

在之前deno的issue裡面出現了一個問題:

大前端的發展趨勢

大前端是一群最能或者說最需要折騰的開發者:

  • 客戶端開發者:從Android到iOS,或者從iOS到Android,到RN,甚至現在越來越多的客戶端開發者接觸前端相關知識(Vue、React、Angular、小程式);
  • 前端開發者:從jQuery到AngularJS,到三大框架並行:Vue、React、Angular,還有小程式,甚至現在也要接觸客戶端開發(比如RN、Flutter);
  • 目前又面臨著不僅僅學習ES的特性,還要學習TypeScript;
  • 新框架的出現,我們又需要學習新框架的特性,比如vue3.x、react18等等;

但是每一樣技術的出現都會讓人驚喜,因為他必然是解決了之前技術的某一個痛點的,而TypeScript真是解決了JavaScript存在的很多設計缺陷,尤其是關於型別檢測的。

並且從開發者長遠的角度來看,學習TypeScript有助於我們前端程式設計師培養 型別思維,這種思維方式對於完成大型專案尤為重要。

TypeScript環境搭建

TypeScript的編譯環境

在前面我們提到過,TypeScript最終會被編譯成JavaScript來執行,所以我們需要搭建對應的環境:

  • 我們需要在電腦上安裝TypeScript,這樣就可以通過TypeScript的Compiler將其編譯成JavaScript;

  • 所以,我們需要先可以先進行全域性的安裝:

    # 安裝命令
    npm install typescript -g
    
    #檢視版本
    tsc --version
    

TypeScript的執行環境

如果我們每次為了檢視TypeScript程式碼的執行效果,都通過經過兩個步驟的話就太繁瑣了:

  • 第一步:通過tsc編譯TypeScript到JavaScript程式碼;
  • 第二步:在瀏覽器或者Node環境下執行JavaScript程式碼;

是否可以簡化這樣的步驟呢?

  • 比如編寫了TypeScript之後可以直接執行在瀏覽器上?
  • 比如編寫了TypeScript之後,直接通過node的命令來執行?

上面我提到的兩種方式,可以通過兩個解決方案來完成:

  • 方式一:通過webpack,配置本地的TypeScript編譯環境和開啟一個本地服務,可以直接執行在瀏覽器上;
  • 方式二:通過ts-node庫,為TypeScript的執行提供執行環境;

方式一:webpack配置

webpack配置TS環境

先裝包

npm install webpack webpack-cli webpack-dev-server typescript ts-loader html-webpack-plugin -D

這裡面有個坑,就是webpakc-dev-server已經更新到4.x的版本了,如果你的Node.js版本比較低會導致報錯,建議更新最新的Node.js版本

如果不想安裝最新Node.js版本,建議使用下面寫好的package.json檔案,直接npm i安裝依賴即可。

{
  "name": "03_webpack_ts",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack",
    "serve": "webpack serve"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "html-webpack-plugin": "^5.3.2",
    "ts-loader": "^9.2.3",
    "typescript": "^4.3.5",
    "webpack": "^5.44.0",
    "webpack-cli": "^4.7.2",
    "webpack-dev-server": "^3.11.2"
  }
}

在專案根目錄建立src目錄和index.html還有webpack.config.js配置檔案,然後將下面程式碼覆蓋到webpack.config.js中。

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body></body>
</html>

webpack.config.js

const path = require('path')
const HtmlWebpackPlugin = require('html-webpack-plugin')

module.exports = {
  mode: "development",
  entry: "./src/main.ts",
  output: {
    path: path.resolve(__dirname, "./dist"),
    filename: "bundle.js"
  },
  devServer: {
  },
  resolve: {
    extensions: [".ts", ".js", ".cjs", ".json"]
  },
  module: {
    rules: [
      {
        test: /\.ts$/,
        loader: 'ts-loader'
      }
    ]
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: "./index.html"
    })
  ]
}

具體的各項配置是什麼意思,參考我之前寫的 webpack 筆記

src目錄中建立main.ts檔案,寫點 TS 程式碼。

main.ts

interface IPerson {
  name: string
  age: number
}

class Person implements IPerson {
  name: string
  age: number
  constructor(name: string, age: number) {
    this.name = name
    this.age = age
  }

  getUserInfo() {
    return `[*] my name is: ${this.name}, i am ${this.age} years old.`
  }
}

const p1 = new Person('惠蘭', 18)
console.log(p1)
console.log(p1.getUserInfo())

此時如果直接npm run serve會報錯的,因為還缺少tsconfig.json配置檔案,這裡直接通過tsc --init建立,然後npm run serve執行就能跑起來了。

最終專案目錄結構:

執行效果:

使用ts-node執行TS指令碼

方式二:安裝ts-node:npm install ts-node -g

另外ts-node需要依賴 tslib 和 @types/node 兩個包:npm install tslib @types/node -g

現在,我們可以直接通過 ts-node 來執行TypeScript的程式碼:ts-node math.ts

TS資料型別

變數的宣告

我們已經強調過很多次,在TypeScript中定義變數需要指定 識別符號 的型別。

所以完整的宣告格式如下:

  • 聲明瞭型別後TypeScript就會進行型別檢測,宣告的型別可以稱之為型別註解;

    var/let/const 識別符號: 資料型別 = 賦值;

比如我們宣告一個message,完整的寫法如下:

  • 注意:這裡的string是小寫的,和String是有區別的

  • string是TypeScript中定義的字串型別,String是ECMAScript中定義的一個類

如果我們給message賦值其他型別的值,那麼就會報錯:

宣告變數的關鍵字

在TypeScript定義變數(識別符號)和ES6之後一致,可以使用var、let、const來定義。

當然,在tslint中並不推薦使用var來宣告變數:

可見,在TypeScript中並不建議再使用var關鍵字了,主要原因和ES6升級後let和var的區別是一樣的,var是沒有塊級作用域的,會引起很多的問題,這裡不再展開探討。

變數的型別推導(推斷)

在開發中,有時候為了方便起見我們並不會在宣告每一個變數時都寫上對應的資料型別,我們更希望可以通過TypeScript本身的特性幫助我們推斷出對應的變數型別:

如果我們給message賦值123:

這是因為在一個變數第一次賦值時,會根據後面的賦值內容的型別,來推斷出變數的型別:

上面的message就是因為後面賦值的是一個string型別,所以message雖然沒有明確的說明,但是依然是一個string型別;

JS和TS的資料型別

我們經常說TypeScript是JavaScript的一個超集:

number型別

數字型別是我們開發中經常使用的型別,TypeScript和JavaScript一樣,不區分整數型別(int)和浮點型(double),統一為number型別。

如果你學習過ES6應該知道,ES6新增了二進位制和八進位制的表示方法,而TypeScript也是支援二進位制、八進位制、十六進位制的表示:

boolean型別

boolean型別只有兩個取值:true和false,非常簡單

string型別

string型別是字串型別,可以使用單引號或者雙引號表示:

同時也支援ES6的模板字串來拼接變數和字串:

Array型別

陣列型別的定義也非常簡單,有兩種方式:

如果新增其他型別到陣列中,那麼會報錯:

Object型別

object物件型別可以用於描述一個物件:

但是從myinfo中我們不能獲取資料,也不能設定資料:

Symbol型別

在ES5中,如果我們希望不可以在物件中新增相同的屬性名稱,可以使用下面的做法:

通常我們的做法是定義兩個不同的屬性名字:比如identity1和identity2。

但是我們也可以通過symbol來定義相同的名稱,因為Symbol函式返回的是不同的值:

null和undefined型別

在 JavaScript 中,undefined 和 null 是兩個基本資料型別。

在TypeScript中,它們各自的型別也是undefined和null,也就意味著它們既是實際的值,也是自己的型別:

TS型別-any

在某些情況下,我們確實無法確定一個變數的型別,並且可能它會發生一些變化,這個時候我們可以使用any型別(類似於Dart語言中的dynamic型別)。

any型別有點像一種討巧的TypeScript手段:

  • 我們可以對any型別的變數進行任何的操作,包括獲取不存在的屬性、方法;
  • 我們給一個any型別的變數賦值任何的值,比如數字、字串的值;

如果對於某些情況的處理過於繁瑣不希望新增規定的型別註解,或者在引入一些第三方庫時,缺失了型別註解,這個時候我們可以使用any:

包括在Vue原始碼中,也會使用到any來進行某些型別的適配;

TS型別-unknown

unknown是TypeScript中比較特殊的一種型別,它用於描述型別不確定的變數。

什麼意思呢?我們來看下面的場景:

TS型別-void

void通常用來指定一個函式是沒有返回值的,那麼它的返回值就是void型別:

  • 我們可以將null和undefined賦值給void型別,也就是函式可以返回null或者undefined

這個函式我們沒有寫任何型別,那麼它預設返回值的型別就是void的,我們也可以顯示的來指定返回值是void:

TS型別-never

never 表示永遠不會有值的型別,比如一個函式:

  • 如果一個函式中是一個死迴圈或者丟擲一個異常,那麼這個函式會返回東西嗎?
  • 不會,那麼寫void型別或者其他型別作為返回值型別都不合適,我們就可以使用never型別;

如果我現在呼叫handleMessage(true)傳了一個布林型別的引數,那麼肯定會有錯誤提示,然後我們會發現message並沒有boolean的引數型別註解,新增之後我們此時可能會忘記在switch..case的結構體中新增對應的判斷條件,編譯的時候肯定會報錯,我們又得根據提示資訊來新增對應判斷處理傳遞進來的布林型別,此時如果使用never資料型別,明確message不會有值,那麼如果我們忘記在switch...case中新增判斷布林型別的程式碼時,肯定會走到default分支,那麼就會對check進行賦值,此時就會有錯誤提示,我們就能馬上知道哪裡有問題,不需要等到ts程式碼編譯運行了才知道錯誤,在複雜的大型專案中還是能感受出來的,例如Vue3

TS型別-tuple

tuple是元組型別,很多語言中也有這種資料型別,比如Python、Swift等。

那麼tuple和陣列有什麼區別呢?

  • 首先,陣列中通常建議存放相同型別的元素,不同型別的元素是不推薦放在陣列中。(可以放在物件或者元組中)

  • 其次,元組中每個元素都有自己特性的型別,根據索引值獲取到的值可以確定對應的型別;

Tuples的應用場景

那麼tuple在什麼地方使用的是最多的呢?

tuple通常可以作為返回的值,在使用的時候會非常的方便;

函式的引數型別

函式是JavaScript非常重要的組成部分,TypeScript允許我們指定函式的引數和返回值的型別

引數的型別註解

  • 宣告函式時,可以在每個引數後新增型別註解,以宣告函式接受的引數型別:

函式的返回值型別

我們也可以新增返回值的型別註解,這個註解出現在函式列表的後面:

和變數的型別註解一樣,我們通常情況下不需要返回型別註解,因為TypeScript會根據 return 返回值推斷函式的返回型別:

某些第三方庫處於方便理解,會明確指定返回型別,但是這個看個人喜好;

匿名函式的引數

匿名函式與函式宣告會有一些不同:

  • 當一個函數出現在TypeScript可以確定該函式會被如何呼叫的地方時;
  • 該函式的引數會自動指定型別;

我們並沒有指定item的型別,但是item是一個string型別:

  • 這是因為TypeScript會根據forEach函式的型別以及陣列的型別推斷出item的型別;
  • 這個過程稱之為上下文型別( contextual typing ),因為函式執行的上下文可以幫助確定引數和返回值的型別;

物件型別

如果我們希望限定一個函式接受的引數是一個物件,這個時候要如何限定呢?

我們可以使用物件型別;

在這裡我們使用了一個物件來作為型別:

  • 在物件我們可以新增屬性,並且告知TypeScript該屬性需要是什麼型別;

  • 屬性之間可以使用 , 或者 ; 來分割,最後一個分隔符是可選的;

  • 每個屬性的型別部分也是可選的,如果不指定,那麼就是any型別;

可選型別

物件型別也可以指定哪些屬性是可選的,可以在屬性的後面新增一個?:

聯合型別

TypeScript的型別系統允許我們使用多種運算子,從現有型別中構建新型別。

我們來使用第一種組合型別的方法:聯合型別(Union Type)

  • 聯合型別是由兩個或者多個其他型別組成的型別;
  • 表示可以是這些型別中的任何一個值;
  • 聯合型別中的每一個型別被稱之為聯合成員(union's members );

使用聯合型別

傳入給一個聯合型別的值是非常簡單的:只要保證是聯合型別中的某一個型別的值即可

  • 但是我們拿到這個值之後,我們應該如何使用它呢?因為它可能是任何一種型別。
  • 比如我們拿到的值可能是string或者number,我們就不能對其呼叫string上的一些方法;

那麼我們怎麼處理這樣的問題呢?

  • 我們需要使用縮小(narrow)聯合(後續我們還會專門講解縮小相關的功能);
  • TypeScript可以根據我們縮小的程式碼結構,推斷出更加具體的型別;

可選型別補充

其實上,可選型別可以看做是 型別 和 undefined 的聯合型別:

類型別名

在前面,我們通過在型別註解中編寫 物件型別 和 聯合型別,但是當我們想要多次在其他地方使用時,就要編寫多次。

比如我們可以給物件型別起一個別名:

型別斷言as

概念

型別斷言:可以用來手動指定一個值的型別。

有時候TypeScript無法獲取具體的型別資訊,這個我們需要使用型別斷言(Type Assertions)。

語法

值 as 型別
或者
<型別>值

推薦使用值 as 型別語法,因為後者在jsx中有問題。

用途

將一個聯合型別斷言為其中一個型別

當 TypeScript 不確定一個聯合型別的變數到底是哪個型別的時候,我們只能訪問此聯合型別的所有型別中共有的屬性或方法

interface Cat {
    name: string;
    run(): void;
}
interface Fish {
    name: string;
    swim(): void;
}

function getName(animal: Cat | Fish) {
    return animal.name;
}

而有時候,我們確實需要在還不確定型別的時候就訪問其中一個型別特有的屬性或方法,比如:

interface Cat {
    name: string;
    run(): void;
}
interface Fish {
    name: string;
    swim(): void;
}

function isFish(animal: Cat | Fish) {
    if (typeof animal.swim === 'function') {
        return true;
    }
    return false;
}

// index.ts:11:23 - error TS2339: Property 'swim' does not exist on type 'Cat | Fish'.
//   Property 'swim' does not exist on type 'Cat'.

上面的例子中,獲取 animal.swim 的時候會報錯。

此時可以使用型別斷言,將 animal 斷言成 Fish

interface Cat {
    name: string;
    run(): void;
}
interface Fish {
    name: string;
    swim(): void;
}

function isFish(animal: Cat | Fish) {
    if (typeof (animal as Fish).swim === 'function') {
        return true;
    }
    return false;
}

這樣就可以解決訪問 animal.swim 時報錯的問題了。

需要注意的是,型別斷言只能夠「欺騙」TypeScript 編譯器,無法避免執行時的錯誤,反而濫用型別斷言可能會導致執行時錯誤:

interface Cat {
    name: string;
    run(): void;
}
interface Fish {
    name: string;
    swim(): void;
}

function swim(animal: Cat | Fish) {
    (animal as Fish).swim();
}

const tom: Cat = {
    name: 'Tom',
    run() { console.log('run') }
};
swim(tom);
// Uncaught TypeError: animal.swim is not a function`

上面的例子編譯時不會報錯,但在執行時會報錯:

Uncaught TypeError: animal.swim is not a function`

原因是 (animal as Fish).swim() 這段程式碼隱藏了 animal 可能為 Cat 的情況,將 animal 直接斷言為 Fish 了,而 TypeScript 編譯器信任了我們的斷言,故在呼叫 swim() 時沒有編譯錯誤。

可是 swim 函式接受的引數是 Cat | Fish,一旦傳入的引數是 Cat 型別的變數,由於 Cat 上沒有 swim 方法,就會導致執行時錯誤了。

總之,使用型別斷言時一定要格外小心,儘量避免斷言後呼叫方法或引用深層屬性,以減少不必要的執行時錯誤。

其他例子

比如我們通過 document.getElementById,TypeScript只知道該函式會返回 HTMLElement ,但並不知道它具體的型別:

TypeScript只允許型別斷言轉換為 更具體 或者 不太具體 的型別版本,此規則可防止不可能的強制轉換:

更具體的意思就是 string,number這種型別;

不太具體的意思就是 unknown這種型別。

非空型別斷言!

當我們編寫下面的程式碼時,在執行ts的編譯階段會報錯:

這是因為傳入的message有可能是為undefined的,這個時候是不能執行方法的;

但是,我們確定傳入的引數是有值的,這個時候我們可以使用非空型別斷言:

非空斷言使用的是 ! ,表示可以確定某個識別符號是有值的,跳過ts在編譯階段對它的檢測;

如果是明確有值,那為什麼還要設定成可選引數啊,直接強制要求傳參不就行了?

這個知識點需要多看案例反覆理解。

可選鏈的使用

可選鏈事實上並不是TypeScript獨有的特性,它是ES11(ES2020)中增加的特性:

  • 可選鏈使用可選鏈操作符 ?.;
  • 它的作用是當物件的屬性不存在時,會短路,直接返回undefined,如果存在,那麼才會繼續執行;
  • 雖然可選鏈操作是ECMAScript提出的特性,但是可以在TypeScript一起使用;

??和!!的作用

有時候我們還會看到 !! 和 ?? 操作符,這些都是做什麼的呢?

!!操作符:

  • 將一個其他型別轉換成boolean型別;
  • 類似於Boolean(變數)的方式;

??操作符:

  • 它是ES11增加的新特性;
  • 空值合併操作符(??)是一個邏輯操作符,當操作符的左側是 null 或者 undefined 時,返回其右側運算元,否則返回左側運算元;

??操作符類似於||操作符,都可以給某個變數設定預設值,但是還是有區別的,短路或運算子是根據左側是否為布林型別的值來決定返回值的,?? 運算子是根據左側是否為 null 或者 undefined 來決定返回值的。

字面量型別

除了前面我們所講過的型別之外,也可以使用字面量型別(literal types):

那麼這樣做有什麼意義呢?

預設情況下這麼做是沒有太大的意義的,但是我們可以將多個型別聯合在一起;

傳參的時候必須是定義的字面量型別的值,否則報錯。

TypeScript 有點繁瑣,可能是我習慣了"自由"。

字面量推理

這是因為我們的物件在進行字面量推理的時候,info其實是一個 {url: string, method: string},所以我們沒辦法將
一個 string 賦值給一個 字面量 型別。

解決方法1:就是通過型別斷言確定具體的型別,確定 method 型別為 'GET' 型別;

解決方法2:增加as const 表示會對info物件進行字面量型別推理,這是另外一種解決方法。

型別縮小

什麼是型別縮小呢?

  • 型別縮小的英文是 Type Narrowing;
  • 我們可以通過類似於 typeof padding === "number" 的判斷語句,來改變TypeScript的執行路徑;
  • 在給定的執行路徑中,我們可以縮小比宣告時更小的型別,這個過程稱之為 縮小;
  • 而我們編寫的 typeof padding === "number 可以稱之為 型別保護(type guards);

常見的型別保護有如下幾種:

  • typeof
  • 平等縮小(比如=、!
  • instanceof
  • in
  • ...

typeof

在 TypeScript 中,檢查返回的值typeof是一種型別保護:因為 TypeScript 對如何typeof操作不同的值進行編碼。

平等縮小

我們可以使用Switch或者相等的一些運算子來表達相等性(比如=, !, ==, and != ):

或者使用 if (direction === 'left') {console.log("呼叫left方法")}

instanceof

JavaScript 有一個運算子來檢查一個值是否是另一個值的“例項”:

in

Javascript 有一個運算子,用於確定物件是否具有帶名稱的屬性:in運算子

如果指定的屬性在指定的物件或其原型鏈中,則in 運算子返回true;

函式

TypeScript函式型別

在JavaScript開發中,函式是重要的組成部分,並且函式可以作為一等公民(可以作為引數,也可以作為返回值進行傳遞)。

那麼在使用函式的過程中,函式是否也可以有自己的型別呢?

我們可以編寫函式型別的表示式(Function Type Expressions),來表示函式型別;

TypeScript函式型別解析

在上面的語法中 (num1: number, num2: number) => void,代表的就是一個函式型別:

  • 接收兩個引數的函式:num1和num2,並且都是number型別;
  • 並且這個函式是沒有返回值的,所以是void;

在某些語言中,可能引數名稱num1和num2是可以省略,但是TypeScript是不可以的:

引數的可選型別

我們可以指定某個引數是可選的:

這個時候這個引數x依然是有型別的,它是什麼型別呢? number | undefined

另外可選型別需要在必傳引數的後面:

預設引數

從ES6開始,JavaScript是支援預設引數的,TypeScript也是支援預設引數的:

這個時候y的型別其實是 undefined 和 number 型別的聯合。

剩餘引數

從ES6開始,JavaScript也支援剩餘引數,剩餘引數語法允許我們將一個不定數量的引數放到一個數組中。

可推導的this型別

this是JavaScript中一個比較難以理解和把握的知識點:

我在公眾號也有一篇文章專門講解this:https://mp.weixin.qq.com/s/hYm0JgBI25grNG_2sCRlTA;

因為this在不同的情況下會繫結不同的值,所以對於它的型別就更難把握了;

那麼,TypeScript是如何處理this呢?我們先來看一個例子:

上面的程式碼是可以正常執行的,也就是TypeScript在編譯時,認為我們的this是可以正確去使用的:

TypeScript認為函式 sayHello 有一個對應的this的外部物件 info,所以在使用時,就會把this當做該物件。

不確定的this型別

但是對於某些情況來說,我們並不知道this到底是什麼?

這段程式碼執行會報錯的:

  • 這裡我們再次強調一下,TypeScript進行型別檢測的目的是讓我們的程式碼更加的安全;
  • 所以這裡對於 sayHello 的呼叫來說,我們雖然將其放到了info中,通過info去呼叫,this依然是指向info物件的;
  • 但是對於TypeScript編譯器來說,這個程式碼是非常不安全的,因為我們也有可能直接呼叫函式,或者通過別的物件來呼叫函式;

指定this的型別

這個時候,通常TypeScript會要求我們明確的指定this的型別:

呼叫sayHello函式的時候必須要求this是指向包含name屬性的物件的,必須被包含name屬性的物件呼叫,否則編譯不通過。

函式的過載

在TypeScript中,如果我們編寫了一個add函式,希望可以對字串和數字型別進行相加,應該如何編寫呢?

我們可能會這樣來編寫,但是其實是錯誤的:

那麼這個程式碼應該如何去編寫呢?

  • 在TypeScript中,我們可以去編寫不同的過載簽名( overload signatures )來表示函式可以以不同的方式進行呼叫;
  • 一般是編寫兩個或者以上的過載簽名,再去編寫一個通用的函式以及實現;

函式過載:函式名稱相同,但是引數不同的幾個函式,就是函式過載。

使用了函式過載,那麼在實現函式體時函式型別應該使用any。

能使用聯合型別就直接使用聯合型別,如果聯合型別實現起來麻煩,才考慮使用函式過載。

sum函式的過載

比如我們對sum函式進行重構:

在我們呼叫sum的時候,它會根據我們傳入的引數型別來決定執行函式體時,到底執行哪一個函式的過載簽名;

但是注意,有實現體的函式,是不能直接被呼叫的:

聯合型別和過載

我們現在有一個需求:定義一個函式,可以傳入字串或者陣列,獲取它們的長度。

這裡有兩種實現方案:

  • 方案一:使用聯合型別來實現;
  • 方案二:實現函式過載來實現;

在開發中我們選擇使用哪一種呢?儘量選擇使用聯合型別來實現!

認識類的使用

在早期的JavaScript開發中(ES5)我們需要通過函式和原型鏈來實現類和繼承,從ES6開始,引入了class關鍵字,可以更加方便的定義和使用類。

TypeScript作為JavaScript的超集,也是支援使用class關鍵字的,並且還可以對類的屬性和方法等進行靜態型別檢測。

實際上在JavaScript的開發過程中,我們更加習慣於函數語言程式設計:

  • 比如React開發中,目前更多使用的函式元件以及結合Hook的開發模式;
  • 比如在Vue3開發中,目前也更加推崇使用 Composition API;

但是在封裝某些業務的時候,類具有更強大封裝性,所以我們也需要掌握它們。

類的定義我們通常會使用class關鍵字:

  • 在面向物件的世界裡,任何事物都可以使用類的結構來描述;
  • 類中包含特有的屬性和方法;

類的定義

我們來定義一個Person類:

使用class關鍵字來定義一個類;

我們可以宣告一些類的屬性:在類的內部宣告類的屬性以及對應的型別

  • 如果型別沒有宣告,那麼它們預設是any的;
  • 我們也可以給屬性設定初始化值;
  • 在預設的strictPropertyInitialization模式下面我們的屬性是必須初始化的,如果沒有初始化,那麼編譯時就會報錯;
  • 如果我們在strictPropertyInitialization模式下確實不希望給屬性初始化,可以使用 name!: string語法;
  • 類可以有自己的建構函式constructor,當我們通過new關鍵字建立一個例項時,建構函式會被呼叫;
  • 建構函式不需要返回任何值,預設返回當前創建出來的例項;
  • 類中可以有自己的函式,定義的函式稱之為方法;

TS嚴格模式下定義的類時,一定要對類中的屬性初始化值,否則會報錯,可以在定義的時候初始化,也可以在constructor中初始化。

類的繼承

面向物件的其中一大特性就是繼承,繼承不僅僅可以減少我們的程式碼量,也是多型的使用前提。

我們使用extends關鍵字來實現繼承,子類中使用super來訪問父類。

我們來看一下Student類繼承自Person:

  • Student類可以有自己的屬性和方法,並且會繼承Person的屬性和方法;
  • 在建構函式中,我們可以通過super來呼叫父類的構造方法,對父類中的屬性進行初始化;

`

類的成員修飾符

在TypeScript中,類的屬性和方法支援三種修飾符: public、private、protected

  • public 修飾的是在任何地方可見、公有的屬性或方法,預設編寫的屬性就是public的;
  • private 修飾的是僅在同一類中可見、私有的屬性或方法;
  • protected 修飾的是僅在類自身及子類中可見、受保護的屬性或方法;

public是預設的修飾符,也是可以直接訪問的,我們這裡來演示一下protected和private。

只讀屬性readonly

如果有一個屬性我們不希望外界可以任意的修改,只希望確定值後直接使用,那麼可以使用readonly:

getters/setters

在前面一些私有屬性我們是不能直接訪問的,或者某些屬性我們想要監聽它的獲取(getter)和設定(setter)的過程,這個時候我們可以使用存取器。

靜態成員

靜態方法

使用 static 修飾符修飾的方法稱為靜態方法,它們不需要例項化,而是直接通過類來呼叫:

class Animal {
  name: string;
  constructor(name) {
    this.name = name
  }
  static isAnimal(a) {
    return a instanceof Animal
  }
}
let a = new Animal('Jack')
console.log(a.name)
// console.log(a.isAnimal) // 無法通過例項訪問靜態成員
console.log(Animal.isAnimal(a))

靜態屬性

ES7 提案中,可以使用 static 定義一個靜態屬性:

class Animal {
  static num = 42;

  constructor() {
    // ...
  }
}

console.log(Animal.num); // 42

ES7 中類的用法

ES6 中例項的屬性只能通過建構函式中的 this.xxx 來定義,ES7 提案中可以直接在類裡面定義:

class Animal {
  name = 'Jack';

  constructor() {
    // ...
  }
}

let a = new Animal();
console.log(a.name); // Jack

抽象類abstract

我們知道,繼承是多型使用的前提。

  • 所以在定義很多通用的呼叫介面時, 我們通常會讓呼叫者傳入父類,通過多型來實現更加靈活的呼叫方式。
  • 但是,父類本身可能並不需要對某些方法進行具體的實現,所以父類中定義的方法,,我們可以定義為抽象方法。

什麼是 抽象方法? 在TypeScript中沒有具體實現的方法(沒有方法體),就是抽象方法。

  • 抽象方法,必須存在於抽象類中;
  • 抽象類是使用abstract宣告的類;

抽象類有如下的特點:

  • 抽象類是不能被例項化的(也就是不能通過new建立)
  • 抽象方法必須被子類實現,否則該類必須是一個抽象類;

抽象類演練

/* 
  通過抽象類實現計算不同幾何圖形的面積
*/

function makeArea(shape: Shape) {
  return shape.getArea()
}


// 建立幾何圖形抽象類
abstract class Shape {
  // 建立計算幾何圖形面積的抽象方法
  abstract getArea(): number
}

// 子類繼承抽象類並且實現getArea抽象方法
class Rectangle extends Shape {
  private width: number
  private height: number
  constructor(width, height) {
    super()
    this.width = width
    this.height = height
  }
  getArea() {
    return this.width * this.height
  }
}

class Circle extends Shape {
  private r: number
  constructor(r: number) {
    super()
    this.r = r
  }

  getArea() {
    return this.r * this.r * 3.14
  }
}

const rectangle = new Rectangle(20, 30)
const circle = new Circle(10)

console.log(makeArea(rectangle))
console.log(makeArea(circle))


類的型別

類本身也是可以作為一種資料型別的:

class Person {
  name: string = "123"
  eating() {
    console.log('eating')
  }
}

const p = new Person()
p.eating()
const p1: Person = {
  name: "Alex",
  eating() {
    console.log(`${this.name} eating`)
  }
}
console.log(p1.name)
p1.eating()

function printPerson(p: Person) {
  console.log(p.name)
}

printPerson(new Person())
printPerson({ name: "Jack", eating: function () { } })

export { }

介面

在 TypeScript 中,我們使用介面(Interfaces)來定義物件的型別。

什麼是介面

在面嚮物件語言中,介面(Interfaces)是一個很重要的概念,它是對行為的抽象,而具體如何行動需要由類(classes)去實現(implement)。

TypeScript 中的介面是一個非常靈活的概念,除了可用於對類的一部分行為進行抽象以外,也常用於對「物件的形狀(Shape)」進行描述。

介面的宣告

在前面我們通過type可以用來宣告一個物件型別:

物件的另外一種宣告方式就是通過介面來宣告:

他們在使用上的區別,我們後續再來說明。

可選屬性

介面中我們也可以定義可選屬性:

只讀屬性

介面中也可以定義只讀屬性:

這樣就意味著我們再初始化之後,這個值是不可以被修改的;

索引型別

前面我們使用interface來定義物件型別,這個時候其中的屬性名、型別、方法都是確定的,但是有時候我們會遇到類似下面的物件:

// 通過interface來定義索引型別
interface IndexLanguage {
  [index: number]: string
}

const frontLanguage: IndexLanguage = {
  0: "HTML",
  1: "CSS",
  2: "JavaScript",
  3: "Vue"
}


interface ILanguageYear {
  [name: string]: number
}

const languageYear: ILanguageYear = {
  "C": 1972,
  "Java": 1995,
  "JavaScript": 1996,
  "TypeScript": 2014
}

函式型別

前面我們都是通過interface來定義物件中普通的屬性和方法的,實際上它也可以用來定義函式型別:

當然,除非特別的情況,還是推薦大家使用類型別名來定義函式:

// 使用type定義函式型別
// type CalcFn = (n1: number, n2: number) => number

// 使用介面定義
interface CalcFn {
  // (引數型別): 返回值型別
  (n1: number, n2: number): number
}

function calc(num1: number, num2: number, calcFn: CalcFn) {
  return calcFn(num1, num2)
}

const add: CalcFn = (num1, num2) => {
  return num1 + num2
}

const res = calc(20, 30, add)
console.log("[*] res: ", res)

介面繼承

介面和類一樣是可以進行繼承的,也是使用extends關鍵字:

並且我們會發現,介面是支援多繼承的(類不支援多繼承)

某個介面繼承多個介面之後,在建立物件時必須將繼承的多個介面的屬性和方法全部實現。

交叉型別

前面我們學習了聯合型別:聯合型別表示多個型別中一個即可。

還有另外一種型別合併,就是交叉型別(Intersection Types):

  • 交叉類似表示需要滿足多個型別的條件;
  • 交叉型別使用 & 符號;

我們來看下面的交叉型別:

  • 表達的含義是number和string要同時滿足;
  • 但是有同時滿足是一個number又是一個string的值嗎?其實是沒有的,所以MyType其實是一個never型別;

交叉型別的應用

所以,在開發中,我們進行交叉時,通常是對物件型別進行交叉的:

// 一種組合型別的方式:聯合型別
type WhyType = number | string
type Direction = "left" | "right" | "center"

// 另一種組合型別的方式:交叉型別
type WType = number & string

interface ISwim {
  swimming: () => void
}

interface IFly {
  flying: () => void
}

type MyType1 = ISwim | IFly
type MyType2 = ISwim & IFly

const obj1: MyType1 = {
  flying() { }
}

const obj2: MyType2 = {
  flying() { },
  // 只寫一個會報錯,ISwim和IFly中的方法全部都得實現
  swimming() { }
}


export { }

介面的實現

介面定義後,也是可以被類實現的:

  • 如果被一個類實現,那麼在之後需要傳入介面的地方,都可以將這個類的例項物件傳入;
  • 這就是面向介面開發;

interface和type區別

我們會發現interface和type都可以用來定義物件型別,那麼在開發中定義物件型別時,到底選擇哪一個呢?

  • 如果是定義非物件型別,通常推薦使用type,比如Direction、Alignment、一些Function;

如果是定義物件型別,那麼他們是有區別的:

  • interface 可以重複的對某個介面來定義屬性和方法;
  • 而type定義的是別名,別名是不能重複的;
interface IFoo {
  name: string
}

interface IFoo {
  age: number
}

const foo: IFoo = {
  name: "Jack",
  age: 10
}

/* 
  interface的名稱可以重複定義,每次定義都會合並之前定義的介面,從而實現拓展介面中的屬性
*/

// type IBar = {
//   name: string
//   age: number
// }

// type IBar = {
  
// }

/* 
  type別名不能重複定義,因此無法實現合併屬性,拓展功能
*/

字面量賦值

我們來看下面的程式碼:

直接賦值一個物件給p,如果這個物件裡面有介面中未定義的屬性,則 TS 會直接報錯的。

下面將物件先賦值給一個變數,然後再賦值給obj就能欺騙 TS,雖然不會報錯,但是會進行擦除操作,當obj.age獲取值的時候還是會報錯。

另一個案例:

interface IPerson {
  name: string
  age: number
  isGirl: boolean
}

const info = {
  name: "Alex",
  age: 12,
  isGirl: false,
  address: "廣州市"
}

const p: IPerson = info
console.log("info: ", info)
console.log("p: ", p)
// 拿不到address屬性,因為ts在進行字面量賦值的時候會進行freshness擦除操作,將address刪除了
// console.log("p.address: ", p.address)

這是因為TypeScript在字面量直接賦值的過程中,為了進行型別推導會進行嚴格的型別限制。

但是之後如果我們是將一個 變數識別符號 賦值給其他的變數時,會進行freshness擦除操作。

這是一個值得注意的細節,也算是一個小坑。

TypeScript列舉型別

列舉型別是為數不多的TypeScript特性有的特性之一:

  • 列舉其實就是將一組可能出現的值,一個個列舉出來,定義在一個型別中,這個型別就是列舉型別;
  • 列舉允許開發者定義一組命名常量,常量可以是數字、字串型別;

列舉型別的值

列舉型別預設是有值的,比如上面的列舉,預設值是這樣的:

當然,我們也可以給列舉其他值:

這個時候會從100進行遞增;

我們也可以給他們賦值其他的型別:

泛型

泛型(Generics)是指在定義函式、介面或類的時候,不預先指定具體的型別,而在使用的時候再指定型別的一種特性。

比如我們可以通過函式來封裝一些API,通過傳入不同的函式引數,讓函式幫助我們完成不同的操作;但是對於引數的型別是否也可以引數化呢?

什麼是型別的引數化?我們來提一個需求:封裝一個函式,傳入一個引數,並且返回這個引數;

如果我們是TypeScript的思維方式,要考慮這個引數和返回值的型別需要一致:

上面的程式碼雖然實現了,但是不適用於其他型別,比如string、boolean、Person等型別:

泛型實現型別引數化

雖然any是可以的, 但是定義為any的時候,我們其實已經丟失了型別資訊:

  • 比如我們傳入的是一個number,那麼我們希望返回的可不是any型別,而是number型別;

  • 所以,我們需要在函式中可以捕獲到引數的型別是number,並且同時使用它來作為返回值的型別;

我們需要在這裡使用一種特性的變數 - 型別變數(type variable),它作用於型別,而不是值:

這裡我們可以使用兩種方式來呼叫它:

方式一:通過 <型別> 的方式將型別傳遞給函式;

方式二:通過型別推到,自動推到出我們傳入變數的型別:

在這裡會推匯出它們是 字面量型別 的,因為字面量型別對於我們的函式也是適用的

泛型的基本補充

當然我們也可以傳入多個型別:

平時在開發中我們可能會看到一些常用的名稱:

  • T:Type的縮寫,型別
  • K、V:key和value的縮寫,鍵值對
  • E:Element的縮寫,元素
  • O:Object的縮寫,物件
function foo<T, E, O>(arg1: T, arg2: E, arg3?: O, ...args: T[]) {
  console.log(arg1, arg2, arg3)
  console.log("剩餘引數: ", args)
}

// 剩餘引數只能使用第一個引數的型別,下面例子的剩餘引數的型別只能是number型別
foo<number, string, { name: string }>(10, 'abc', { name: "Alexander" }, 11, 12, 13)

export { }

泛型介面

interface IPerson<T1, T2> {
  name: T1;
  age: T2
}

const p: IPerson<string, number> = {
  name: "Alexander",
  age: 18
}

console.log(p)

// 設定泛型介面預設型別為 string,如果呼叫介面的時候不傳型別預設就是string,否則就使用傳過來的型別
interface IFoo<T = string> {
  initialValue: T,
  valueList: T[],
  handleValue: (value: T) => void
}

// 此處傳遞了number型別,覆蓋IFoo預設的string型別
const foo: IFoo<number> = {
  initialValue: 0,
  valueList: [0, 1, 2, 3],
  handleValue: function (value) {
    console.log(value)
  }
}

console.log(foo)
foo.handleValue(123)

泛型類

class Point<T> {
  x: T
  y: T
  z: T

  constructor(x: T, y: T, z: T) {
    this.x = x
    this.y = y
    this.z = z
  }
}

// 根據型別推導得出型別為string
const p1 = new Point('string', '666', 'true')
// 手動傳遞型別
const p2 = new Point<string>('string', '666', 'true')
// p3是一個Point型別,建構函式的引數為string型別
const p3: Point<string> = new Point('string', '666', 'true')
console.log(p1)
console.log(p2)
console.log(p3)

泛型約束

有時候我們希望傳入的型別有某些共性,但是這些共性可能不是在同一種類型中:

比如string和array都是有length的,或者某些物件也是會有length屬性的;

那麼只要是擁有length的屬性都可以作為我們的引數型別,那麼應該如何操作呢?

interface ILength {
  length: number
}

// arg引數必須有number型別的length屬性;泛型通過extends關鍵字配合介面就可以實現型別約束;
function getLength<T extends ILength>(arg: T) {
  return arg.length
}

console.log(getLength("abc"));
console.log(getLength(["abc", "cba"]));
console.log(getLength({ length: 100 }));

模組化開發

TypeScript支援兩種方式來控制我們的作用域:

  • 模組化:每個檔案可以是一個獨立的模組,支援ES Module,也支援CommonJS;
  • 名稱空間:通過namespace來宣告一個名稱空間

名稱空間

名稱空間在TypeScript早期時,稱之為內部模組,主要目的是將一個模組內部再進行作用域的劃分,防止一些命名衝突的問題。

/* 
  預設情況下我們不能定義2個名稱相同的模組,但是特殊情況我們希望定義怎麼辦?
  此時可以使用namespace關鍵字進行包裹。
*/

/* export function format(time: string) {
  return "2021-12-13 12:36:15"
}

export function format(price: number) {
  return price.toFixed(2)
} */

export namespace time {
  // 格式化時間
  export function format(time: number) {
    return new Date(time)
  }
}

export namespace price {
  // 格式化價格
  export function format(price: number) {
    return price.toFixed(2)
  }
}

呼叫:

console.log(time.format(Date.now()))
console.log(price.format(19))

型別的查詢

之前我們所有的typescript中的型別,幾乎都是我們自己編寫的,但是我們也有用到一些其他的型別:

大家是否會奇怪,我們的HTMLImageElement型別來自哪裡呢?甚至是document為什麼可以有getElementById的方法呢?

其實這裡就涉及到typescript對型別的管理和查詢規則了。

我們這裡先給大家介紹另外的一種typescript檔案:.d.ts檔案

  • 我們之前編寫的typescript檔案都是 .ts 檔案,這些檔案最終會輸出 .js 檔案,也是我們通常編寫程式碼的地方;
  • 還有另外一種檔案 .d.ts 檔案,它是用來做型別的宣告(declare)。 它僅僅用來做型別檢測,告知typescript我們有哪些型別;

那麼typescript會在哪裡查詢我們的型別宣告呢?

  • 內建型別宣告;
  • 外部定義型別宣告;
  • 自己定義型別宣告;

內建型別宣告

內建型別宣告是typescript自帶的、幫助我們內建了JavaScript執行時的一些標準化API的宣告檔案;

包括比如Math、Date等內建型別,也包括DOM API,比如Window、Document等;

內建型別宣告通常在我們安裝typescript的環境中會帶有的;

https://github.com/microsoft/TypeScript/tree/main/lib

外部定義型別宣告和自定義宣告

外部型別宣告通常是我們使用一些庫(比如第三方庫)時,需要的一些型別宣告。

這些庫通常有兩種型別宣告方式:

  1. 在自己庫中進行型別宣告(編寫.d.ts檔案),比如axios

  2. 通過社群的一個公有庫DefinitelyTyped存放型別宣告檔案

    該庫的GitHub地址:https://github.com/DefinitelyTyped/DefinitelyTyped/

    該庫查詢宣告安裝方式的地址:https://www.typescriptlang.org/dt/search?search=

    比如我們安裝react的型別宣告: npm i @types/react --save-dev

什麼情況下需要自己來定義宣告檔案呢?

  • 我們使用的第三方庫是一個純的JavaScript庫,沒有對應的宣告檔案;比如lodash
  • 我們給自己的程式碼中宣告一些型別,方便在其他地方直接進行使用;

宣告變數-函式-類

index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body></body>

  <script>
    let username = "Alexander";
    let age = 18;
    let isLogin = true;

    function getPwd() {
      return Date.now();
    }

    class Person {
      constructor(name, age) {
        this.name = name;
        this.age = age;
      }
    }
  </script>
</html>

main.ts

console.log(username)
console.log(age)
console.log(isLogin)
console.log(getPwd())
const p1 = new Person('惠蘭', 18)
console.log(p1)

statement.d.ts

// 宣告變數/函式/類
declare let username: string
declare let age: number
declare let isLogin: boolean
declare function getPwd(): void
declare class Person {
  name: string
  age: number
  constructor(name: string, age: number)
}

宣告模組

我們也可以宣告模組,比如lodash模組預設不能使用的情況,可以自己來宣告這個模組:

// 宣告模組
declare module 'loadsh' {
  export function join(arr: any[]): void
}

宣告模組的語法: declare module '模組名' {}。

在宣告模組的內部,我們可以通過 export 匯出對應庫的類、函式等。

宣告檔案

在某些情況下,我們也可以宣告檔案:

  • 比如在開發vue的過程中,預設是不識別我們的.vue檔案的,那麼我們就需要對其進行檔案的宣告;
  • 比如在開發中我們使用了 jpg 這類圖片檔案,預設typescript也是不支援的,也需要對其進行宣告;

declare名稱空間

比如我們在index.html中直接引入了jQuery:

CDN地址: https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.js

我們可以進行名稱空間的宣告:

// 宣告名稱空間
// declare namespace 宣告(含有子屬性的)全域性物件
declare namespace $ {
  export function ajax(settings: any): void
}

在main.ts中就可以使用了:

tsconfig.json檔案

tsconfig.json是用於配置TypeScript編譯時的配置選項:https://www.typescriptlang.org/tsconfig/

這裡講解幾個比較常見的:

總結

參考資料

程式碼倉庫地址:https://github.com/C4az6/blogs/tree/master/typescript