1. 程式人生 > 實用技巧 >Vue3-01-TypeScript入門

Vue3-01-TypeScript入門

1、TypeScript快速上手

1.1 初識 TypeScript

TypeScript 的介紹

TypeScript是一種由微軟開發的開源、跨平臺的程式語言。它是JavaScript的超集,最終會被編譯為JavaScript程式碼。

2012年10月,微軟釋出了首個公開版本的TypeScript,2013年6月19日,在經歷了一個預覽版之後微軟正式釋出了正式版TypeScript

TypeScript的作者是安德斯·海爾斯伯格,C#的首席架構師。它是開源和跨平臺的程式語言。

TypeScript擴充套件了JavaScript的語法,所以任何現有的JavaScript程式可以執行在TypeScript環境中。

TypeScript是為大型應用的開發而設計,並且可以編譯為JavaScript。

TypeScript 是 JavaScript 的一個超集,主要提供了型別系統和對 ES6+ 的支援**,它由 Microsoft 開發,程式碼開源於 GitHub 上

TypeScript 是 JavaScript 的一個超集,主要提供了型別系統對 ES6+ 的支援,它由 Microsoft 開發,程式碼開源於 GitHub

TypeScript 的特點

TypeScript 主要有 3 大特點:

  • 始於JavaScript,歸於JavaScript

TypeScript 可以編譯出純淨、 簡潔的 JavaScript 程式碼,並且可以執行在任何瀏覽器上、Node.js 環境中和任何支援 ECMAScript 3(或更高版本)的JavaScript 引擎中。

  • 強大的型別系統

型別系統允許 JavaScript 開發者在開發 JavaScript 應用程式時使用高效的開發工具和常用操作比如靜態檢查和程式碼重構。

  • 先進的 JavaScript

TypeScript 提供最新的和不斷髮展的 JavaScript 特性,包括那些來自 2015 年的 ECMAScript 和未來的提案中的特性,比如非同步功能和 Decorators,以幫助建立健壯的元件。

總結

TypeScript 在社群的流行度越來越高,它非常適用於一些大型專案,也非常適用於一些基礎庫,極大地幫助我們提升了開發效率和體驗。

1.2 安裝 TypeScript

命令列執行如下命令,全域性安裝 TypeScript:

npm install -g typescript

安裝完成後,在控制檯執行如下命令,檢查安裝是否成功(3.x):

tsc -V 

1.3. 第一個 TypeScript 程式

編寫 TS 程式

src/helloworld.ts

function greeter (person) {
  return 'Hello, ' + person
}

let user = 'Yee'

console.log(greeter(user))

手動編譯程式碼

我們使用了 .ts 副檔名,但是這段程式碼僅僅是 JavaScript 而已。

在命令列上,執行 TypeScript 編譯器:

tsc helloworld.ts

輸出結果為一個 helloworld.js 檔案,它包含了和輸入檔案中相同的 JavsScript 程式碼。

在命令列上,通過 Node.js 執行這段程式碼:

node helloworld.js

控制檯輸出:

Hello, Yee

vscode自動編譯

1). 生成配置檔案tsconfig.json
    tsc --init
2). 修改tsconfig.json配置
    "outDir": "./js",
    "strict": false,    
3). 啟動監視任務: 
    終端 -> 執行任務 -> 監視tsconfig.json

型別註解

接下來讓我們看看 TypeScript 工具帶來的高階功能。 給 person 函式的引數新增 : string 型別註解,如下:

function greeter (person: string) {
  return 'Hello, ' + person
}

let user = 'Yee'

console.log(greeter(user))

TypeScript 裡的型別註解是一種輕量級的為函式或變數新增約束的方式。 在這個例子裡,我們希望 greeter 函式接收一個字串引數。 然後嘗試把 greeter 的呼叫改成傳入一個數組:

function greeter (person: string) {
  return 'Hello, ' + person
}

let user = [0, 1, 2]

console.log(greeter(user))

重新編譯,你會看到產生了一個錯誤:

error TS2345: Argument of type 'number[]' is not assignable to parameter of type 'string'.

類似地,嘗試刪除 greeter 呼叫的所有引數。 TypeScript 會告訴你使用了非期望個數的引數呼叫了這個函式。 在這兩種情況中,TypeScript提供了靜態的程式碼分析,它可以分析程式碼結構和提供的型別註解。

要注意的是儘管有錯誤,greeter.js 檔案還是被建立了。 就算你的程式碼裡有錯誤,你仍然可以使用 TypeScript。但在這種情況下,TypeScript 會警告你程式碼可能不會按預期執行。

介面

讓我們繼續擴充套件這個示例應用。這裡我們使用介面來描述一個擁有 firstNamelastName 欄位的物件。 在 TypeScript 裡,只在兩個型別內部的結構相容,那麼這兩個型別就是相容的。 這就允許我們在實現介面時候只要保證包含了介面要求的結構就可以,而不必明確地使用 implements 語句。

interface Person {
  firstName: string
  lastName: string
}

function greeter (person: Person) {
  return 'Hello, ' + person.firstName + ' ' + person.lastName
}

let user = {
  firstName: 'Yee',
  lastName: 'Huang'
}

console.log(greeter(user))

最後,讓我們使用類來改寫這個例子。 TypeScript 支援 JavaScript 的新特性,比如支援基於類的面向物件程式設計。

讓我們建立一個 User 類,它帶有一個建構函式和一些公共欄位。因為類的欄位包含了介面所需要的欄位,所以他們能很好的相容。

還要注意的是,我在類的宣告上會註明所有的成員變數,這樣比較一目瞭然。

class User {
  fullName: string
  firstName: string
  lastName: string

  constructor (firstName: string, lastName: string) {
    this.firstName = firstName
    this.lastName = lastName
    this.fullName = firstName + ' ' + lastName
  }
}

interface Person {
  firstName: string
  lastName: string
}

function greeter (person: Person) {
  return 'Hello, ' + person.firstName + ' ' + person.lastName
}

let user = new User('Yee', 'Huang')

console.log(greeter(user))

重新執行 tsc greeter.ts,你會看到 TypeScript 裡的類只是一個語法糖,本質上還是 JavaScript 函式的實現。

總結

到這裡,你已經對 TypeScript 有了一個大致的印象,那麼下一章讓我們來一起學習 TypeScript 的一些常用語法吧。

1.4 使用webpack打包TS

下載依賴

yarn add -D typescript
yarn add -D webpack webpack-cli
yarn add -D webpack-dev-server
yarn add -D html-webpack-plugin clean-webpack-plugin
yarn add -D ts-loader
yarn add -D cross-env

入口JS: src/main.ts

// import './01_helloworld'

document.write('Hello Webpack TS!')

index頁面: public/index.html

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

build/webpack.config.js

const {CleanWebpackPlugin} = require('clean-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const path = require('path')

const isProd = process.env.NODE_ENV === 'production' // 是否生產環境

function resolve (dir) {
  return path.resolve(__dirname, '..', dir)
}

module.exports = {
  mode: isProd ? 'production' : 'development',
  entry: {
    app: './src/main.ts'
  },

  output: {
    path: resolve('dist'),
    filename: '[name].[contenthash:8].js'
  },

  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'ts-loader',
        include: [resolve('src')]
      }
    ]
  },

  plugins: [
    new CleanWebpackPlugin({
    }),

    new HtmlWebpackPlugin({
      template: './public/index.html'
    })
  ],

  resolve: {
    extensions: ['.ts', '.tsx', '.js']
  },

  devtool: isProd ? 'cheap-module-source-map' : 'cheap-module-eval-source-map',

  devServer: {
    host: 'localhost', // 主機名
    stats: 'errors-only', // 打包日誌輸出輸出錯誤資訊
    port: 8081,
    open: true
  },
}

配置打包命令

"dev": "cross-env NODE_ENV=development webpack-dev-server --config build/webpack.config.js",
"build": "cross-env NODE_ENV=production webpack --config build/webpack.config.js"

執行與打包

yarn dev
yarn build

2、TypeScript常用語法

2.1 基礎型別

TypeScript 支援與 JavaScript 幾乎相同的資料型別,此外還提供了實用的列舉型別方便我們使用。

布林值

最基本的資料型別就是簡單的 true/false 值,在JavaScript 和 TypeScript 裡叫做 boolean(其它語言中也一樣)。

let isDone: boolean = false;
isDone = true;
// isDone = 2 // error

數字

和 JavaScript 一樣,TypeScript 裡的所有數字都是浮點數。 這些浮點數的型別是 number。 除了支援十進位制和十六進位制字面量,TypeScript 還支援 ECMAScript 2015中引入的二進位制和八進位制字面量。

let a1: number = 10 // 十進位制
let a2: number = 0b1010  // 二進位制
let a3: number = 0o12 // 八進位制
let a4: number = 0xa // 十六進位制

字串

JavaScript 程式的另一項基本操作是處理網頁或伺服器端的文字資料。 像其它語言裡一樣,我們使用 string 表示文字資料型別。 和 JavaScript 一樣,可以使用雙引號(")或單引號(')表示字串。

let name:string = 'tom'
name = 'jack'
// name = 12 // error
let age:number = 12
const info = `My name is ${name}, I am ${age} years old!`

undefined 和 null

TypeScript 裡,undefinednull 兩者各自有自己的型別分別叫做 undefinednull。 它們的本身的型別用處不是很大:

let u: undefined = undefined
let n: null = null

預設情況下 nullundefined 是所有型別的子型別。 就是說你可以把 nullundefined 賦值給 number 型別的變數。

陣列

TypeScript 像 JavaScript 一樣可以運算元組元素。 有兩種方式可以定義陣列。 第一種,可以在元素型別後面接上[],表示由此型別元素組成的一個數組:

let list1: number[] = [1, 2, 3]

第二種方式是使用陣列泛型,Array<元素型別>

let list2: Array<number> = [1, 2, 3]

元組 Tuple

元組型別允許表示一個已知元素數量和型別的陣列,各元素的型別不必相同。 比如,你可以定義一對值分別為 stringnumber 型別的元組。

let t1: [string, number]
t1 = ['hello', 10] // OK
t1 = [10, 'hello'] // Error

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

console.log(t1[0].substring(1)) // OK
console.log(t1[1].substring(1)) // Error, 'number' 不存在 'substring' 方法

列舉

enum 型別是對 JavaScript 標準資料型別的一個補充。 使用列舉型別可以為一組數值賦予友好的名字

enum Color {
  Red,
  Green,
  Blue
}

// 列舉數值預設從0開始依次遞增
// 根據特定的名稱得到對應的列舉數值
let myColor: Color = Color.Green  // 0
console.log(myColor, Color.Red, Color.Blue)

預設情況下,從 0 開始為元素編號。 你也可以手動的指定成員的數值。 例如,我們將上面的例子改成從 1 開始編號:

enum Color {Red = 1, Green, Blue}
let c: Color = Color.Green

或者,全部都採用手動賦值:

enum Color {Red = 1, Green = 2, Blue = 4}
let c: Color = Color.Green

列舉型別提供的一個便利是你可以由列舉的值得到它的名字。 例如,我們知道數值為 2,但是不確定它對映到 Color 裡的哪個名字,我們可以查詢相應的名字:

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

console.log(colorName)  // 'Green'

any

有時候,我們會想要為那些在程式設計階段還不清楚型別的變數指定一個型別。 這些值可能來自於動態的內容,比如來自使用者輸入或第三方程式碼庫。 這種情況下,我們不希望型別檢查器對這些值進行檢查而是直接讓它們通過編譯階段的檢查。 那麼我們可以使用 any 型別來標記這些變數:

let notSure: any = 4
notSure = 'maybe a string'
notSure = false // 也可以是個 boolean

在對現有程式碼進行改寫的時候,any 型別是十分有用的,它允許你在編譯時可選擇地包含或移除型別檢查。並且當你只知道一部分資料的型別時,any 型別也是有用的。 比如,你有一個數組,它包含了不同的型別的資料:

let list: any[] = [1, true, 'free']

list[1] = 100

void

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

/* 表示沒有任何型別, 一般用來說明函式的返回值不能是undefined和null之外的值 */
function fn(): void {
  console.log('fn()')
  // return undefined
  // return null
  // return 1 // error
}

宣告一個 void 型別的變數沒有什麼大用,因為你只能為它賦予 undefinednull

let unusable: void = undefined

object

object 表示非原始型別,也就是除 numberstringboolean之外的型別。

使用 object 型別,就可以更好的表示像 Object.create 這樣的 API。例如:

function fn2(obj:object):object {
  console.log('fn2()', obj)
  return {}
  // return undefined
  // return null
}
console.log(fn2(new String('abc')))
// console.log(fn2('abc') // error
console.log(fn2(String))

聯合型別

聯合型別(Union Types)表示取值可以為多種型別中的一種
需求1: 定義一個一個函式得到一個數字或字串值的字串形式值

function toString2(x: number | string) : string {
  return x.toString()
}

需求2: 定義一個一個函式得到一個數字或字串值的長度

function getLength(x: number | string) {

  // return x.length // error

  if (x.length) { // error
    return x.length
  } else {
    return x.toString().length
  }
}

型別斷言

通過型別斷言這種方式可以告訴編譯器,“相信我,我知道自己在幹什麼”。 型別斷言好比其它語言裡的型別轉換,但是不進行特殊的資料檢查和解構。 它沒有執行時的影響,只是在編譯階段起作用。 TypeScript 會假設你,程式設計師,已經進行了必須的檢查。

型別斷言有兩種形式。 其一是“尖括號”語法, 另一個為 as 語法

/* 
型別斷言(Type Assertion): 可以用來手動指定一個值的型別
語法:
    方式一: <型別>值
    方式二: 值 as 型別  tsx中只能用這種方式
*/

/* 需求: 定義一個函式得到一個字串或者數值資料的長度 */
function getLength(x: number | string) {
  if ((<string>x).length) {
    return (x as string).length
  } else {
    return x.toString().length
  }
}
console.log(getLength('abcd'), getLength(1234))

型別推斷

型別推斷: TS會在沒有明確的指定型別的時候推測出一個型別
有下面2種情況: 1. 定義變數時賦值了, 推斷為對應的型別. 2. 定義變數時沒有賦值, 推斷為any型別

/* 定義變數時賦值了, 推斷為對應的型別 */
let b9 = 123 // number
// b9 = 'abc' // error

/* 定義變數時沒有賦值, 推斷為any型別 */
let b10  // any型別
b10 = 123
b10 = 'abc'

2.2 介面

TypeScript 的核心原則之一是對值所具有的結構進行型別檢查。我們使用介面(Interfaces)來定義物件的型別。介面是物件的狀態(屬性)和行為(方法)的抽象(描述)

介面初探

需求: 建立人的物件, 需要對人的屬性進行一定的約束

id是number型別, 必須有, 只讀的
name是string型別, 必須有
age是number型別, 必須有
sex是string型別, 可以沒有

下面通過一個簡單示例來觀察介面是如何工作的:

/* 
在 TypeScript 中,我們使用介面(Interfaces)來定義物件的型別
介面: 是物件的狀態(屬性)和行為(方法)的抽象(描述)
介面型別的物件
    多了或者少了屬性是不允許的
    可選屬性: ?
    只讀屬性: readonly
*/

/* 
需求: 建立人的物件, 需要對人的屬性進行一定的約束
  id是number型別, 必須有, 只讀的
  name是string型別, 必須有
  age是number型別, 必須有
  sex是string型別, 可以沒有
*/

// 定義人的介面
interface IPerson {
  id: number
  name: string
  age: number
  sex: string
}

const person1: IPerson = {
  id: 1,
  name: 'tom',
  age: 20,
  sex: '男'
}

型別檢查器會檢視物件內部的屬性是否與IPerson介面描述一致, 如果不一致就會提示型別錯誤。

可選屬性

接口裡的屬性不全都是必需的。 有些是隻在某些條件下存在,或者根本不存在。

interface IPerson {
  id: number
  name: string
  age: number
  sex?: string
}

帶有可選屬性的介面與普通的介面定義差不多,只是在可選屬性名字定義的後面加一個 ? 符號。

可選屬性的好處之一是可以對可能存在的屬性進行預定義,好處之二是可以捕獲引用了不存在的屬性時的錯誤。

const person2: IPerson = {
  id: 1,
  name: 'tom',
  age: 20,
  // sex: '男' // 可以沒有
}

只讀屬性

一些物件屬性只能在物件剛剛建立的時候修改其值。 你可以在屬性名前用 readonly 來指定只讀屬性:

interface IPerson {
  readonly id: number
  name: string
  age: number
  sex?: string
}

一旦賦值後再也不能被改變了。

const person2: IPerson = {
  id: 2,
  name: 'tom',
  age: 20,
  // sex: '男' // 可以沒有
  // xxx: 12 // error 沒有在介面中定義, 不能有
}
person2.id = 2 // error

readonly vs const

最簡單判斷該用 readonly 還是 const 的方法是看要把它做為變數使用還是做為一個屬性。 做為變數使用的話用 const,若做為屬性則使用 readonly

函式型別

介面能夠描述 JavaScript 中物件擁有的各種各樣的外形。 除了描述帶有屬性的普通物件外,介面也可以描述函式型別。

為了使用介面表示函式型別,我們需要給介面定義一個呼叫簽名。它就像是一個只有引數列表和返回值型別的函式定義。引數列表裡的每個引數都需要名字和型別。

/* 
介面可以描述函式型別(引數的型別與返回的型別)
*/

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

這樣定義後,我們可以像使用其它介面一樣使用這個函式型別的介面。 下例展示瞭如何建立一個函式型別的變數,並將一個同類型的函式賦值給這個變數。

const mySearch: SearchFunc = function (source: string, sub: string): boolean {
  return source.search(sub) > -1
}

console.log(mySearch('abcd', 'bc'))

類型別

類實現介面

與 C# 或 Java 裡介面的基本作用一樣,TypeScript 也能夠用它來明確的強制一個類去符合某種契約。

/* 
類型別: 實現介面
1. 一個類可以實現多個介面
2. 一個介面可以繼承多個介面
*/

interface Alarm {
  alert(): any;
}

interface Light {
  lightOn(): void;
  lightOff(): void;
}

class Car implements Alarm {
  alert() {
      console.log('Car alert');
  }
}

一個類可以實現多個介面

class Car2 implements Alarm, Light {
  alert() {
    console.log('Car alert');
  }
  lightOn() {
    console.log('Car light on');
  }
  lightOff() {
    console.log('Car light off');
  }
}

介面繼承介面

和類一樣,介面也可以相互繼承。 這讓我們能夠從一個接口裡複製成員到另一個接口裡,可以更靈活地將介面分割到可重用的模組裡。

interface LightableAlarm extends Alarm, Light {

}

2.3 類

對於傳統的 JavaScript 程式我們會使用函式基於原型的繼承來建立可重用的元件,但對於熟悉使用面向物件方式的程式設計師使用這些語法就有些棘手,因為他們用的是基於類的繼承並且物件是由類構建出來的。 從 ECMAScript 2015,也就是 ES6 開始, JavaScript 程式設計師將能夠使用基於類的面向物件的方式。 使用 TypeScript,我們允許開發者現在就使用這些特性,並且編譯後的 JavaScript 可以在所有主流瀏覽器和平臺上執行,而不需要等到下個 JavaScript 版本。

基本示例

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

/* 
類的基本定義與使用
*/

class Greeter {
  // 宣告屬性
  message: string

  // 構造方法
  constructor (message: string) {
    this.message = message
  }

  // 一般方法
  greet (): string {
    return 'Hello ' + this.message
  }
}

// 建立類的例項
const greeter = new Greeter('world')
// 呼叫例項的方法
console.log(greeter.greet())

如果你使用過 C# 或 Java,你會對這種語法非常熟悉。 我們宣告一個 Greeter 類。這個類有 3 個成員:一個叫做 message 的屬性,一個建構函式和一個 greet 方法。

你會注意到,我們在引用任何一個類成員的時候都用了 this。 它表示我們訪問的是類的成員。

後面一行,我們使用 new 構造了 Greeter 類的一個例項。它會呼叫之前定義的建構函式,建立一個 Greeter 型別的新物件,並執行建構函式初始化它。

最後一行通過 greeter 物件呼叫其 greet 方法

繼承

在 TypeScript 裡,我們可以使用常用的面向物件模式。 基於類的程式設計中一種最基本的模式是允許使用繼承來擴充套件現有的類。

看下面的例子:

/* 
類的繼承
*/

class Animal {
  run (distance: number) {
    console.log(`Animal run ${distance}m`)
  }
}

class Dog extends Animal {
  cry () {
    console.log('wang! wang!')
  }
}

const dog = new Dog()
dog.cry() 
dog.run(100) // 可以呼叫從父中繼承得到的方法

這個例子展示了最基本的繼承:類從基類中繼承了屬性和方法。 這裡,Dog 是一個 派生類,它派生自 Animal 基類,通過 extends 關鍵字。 派生類通常被稱作子類,基類通常被稱作超類

因為 Dog 繼承了 Animal 的功能,因此我們可以建立一個 Dog 的例項,它能夠 cry()run()

下面我們來看個更加複雜的例子。

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

  run (distance: number=0) {
    console.log(`${this.name} run ${distance}m`)
  }

}

class Snake extends Animal {
  constructor (name: string) {
    // 呼叫父型別構造方法
    super(name)
  }

  // 重寫父型別的方法
  run (distance: number=5) {
    console.log('sliding...')
    super.run(distance)
  }
}

class Horse extends Animal {
  constructor (name: string) {
    // 呼叫父型別構造方法
    super(name)
  }

  // 重寫父型別的方法
  run (distance: number=50) {
    console.log('dashing...')
    // 呼叫父型別的一般方法
    super.run(distance)
  }

  xxx () {
    console.log('xxx()')
  }
}

const snake = new Snake('sn')
snake.run()

const horse = new Horse('ho')
horse.run()

// 父型別引用指向子型別的例項 ==> 多型
const tom: Animal = new Horse('ho22')
tom.run()

/* 如果子型別沒有擴充套件的方法, 可以讓子型別引用指向父型別的例項 */
const tom3: Snake = new Animal('tom3')
tom3.run()
/* 如果子型別有擴充套件的方法, 不能讓子型別引用指向父型別的例項 */
// const tom2: Horse = new Animal('tom2')
// tom2.run()

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

與前一個例子的不同點是,派生類包含了一個建構函式,它 必須呼叫 super(),它會執行基類的建構函式。 而且,在建構函式裡訪問 this 的屬性之前,我們 一定要呼叫 super()。 這個是 TypeScript 強制執行的一條重要規則。

這個例子演示瞭如何在子類裡可以重寫父類的方法。Snake類和 Horse 類都建立了 run 方法,它們重寫了從 Animal 繼承來的 run 方法,使得 run 方法根據不同的類而具有不同的功能。注意,即使 tom 被宣告為 Animal 型別,但因為它的值是 Horse,呼叫 tom.run(34) 時,它會呼叫 Horse 裡重寫的方法。

sliding...
sn run 5m
dashing...
ho run 50m

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

預設為 public

在上面的例子裡,我們可以自由的訪問程式裡定義的成員。 如果你對其它語言中的類比較瞭解,就會注意到我們在之前的程式碼裡並沒有使用 public 來做修飾;例如,C# 要求必須明確地使用 public 指定成員是可見的。 在 TypeScript 裡,成員都預設為 public

你也可以明確的將一個成員標記成 public。 我們可以用下面的方式來重寫上面的 Animal 類:

理解 private

當成員被標記成 private 時,它就不能在宣告它的類的外部訪問。

理解 protected

protected 修飾符與 private 修飾符的行為很相似,但有一點不同,protected成員在派生類中仍然可以訪問。例如:

/* 
訪問修飾符: 用來描述類內部的屬性/方法的可訪問性
  public: 預設值, 公開的外部也可以訪問
  private: 只能類內部可以訪問
  protected: 類內部和子類可以訪問
*/

class Animal {
  public name: string

  public constructor (name: string) {
    this.name = name
  }

  public run (distance: number=0) {
    console.log(`${this.name} run ${distance}m`)
  }
}

class Person extends Animal {
  private age: number = 18
  protected sex: string = '男'

  run (distance: number=5) {
    console.log('Person jumping...')
    super.run(distance)
  }
}

class Student extends Person {
  run (distance: number=6) {
    console.log('Student jumping...')

    console.log(this.sex) // 子類能看到父類中受保護的成員
    // console.log(this.age) //  子類看不到父類中私有的成員

    super.run(distance)
  }
}

console.log(new Person('abc').name) // 公開的可見
// console.log(new Person('abc').sex) // 受保護的不可見
// console.log(new Person('abc').age) //  私有的不可見

readonly 修飾符

你可以使用 readonly 關鍵字將屬性設定為只讀的。 只讀屬性必須在宣告時或建構函式裡被初始化。

class Person {
  readonly name: string = 'abc'
  constructor(name: string) {
    this.name = name
  }
}

let john = new Person('John')
// john.name = 'peter' // error

引數屬性

在上面的例子中,我們必須在 Person 類裡定義一個只讀成員 name 和一個引數為 name 的建構函式,並且立刻將 name 的值賦給 this.name,這種情況經常會遇到。 引數屬性可以方便地讓我們在一個地方定義並初始化一個成員。 下面的例子是對之前 Person 類的修改版,使用了引數屬性:

class Person2 {
  constructor(readonly name: string) {
  }
}

const p = new Person2('jack')
console.log(p.name)

注意看我們是如何捨棄引數 name,僅在建構函式裡使用 readonly name: string 引數來建立和初始化 name 成員。 我們把宣告和賦值合併至一處。

引數屬性通過給建構函式引數前面新增一個訪問限定符來宣告。使用 private 限定一個引數屬性會宣告並初始化一個私有成員;對於 publicprotected 來說也是一樣。

存取器

TypeScript 支援通過 getters/setters 來擷取對物件成員的訪問。 它能幫助你有效的控制對物件成員的訪問。

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

class Person {
  firstName: string = 'A'
  lastName: string = 'B'
  get fullName () {
    return this.firstName + '-' + this.lastName
  }
  set fullName (value) {
    const names = value.split('-')
    this.firstName = names[0]
    this.lastName = names[1]
  }
}

const p = new Person()
console.log(p.fullName)

p.firstName = 'C'
p.lastName =  'D'
console.log(p.fullName)

p.fullName = 'E-F'
console.log(p.firstName, p.lastName)

靜態屬性

到目前為止,我們只討論了類的例項成員,那些僅當類被例項化的時候才會被初始化的屬性。 我們也可以建立類的靜態成員,這些屬性存在於類本身上面而不是類的例項上。 在這個例子裡,我們使用 static 定義 origin,因為它是所有網格都會用到的屬性。 每個例項想要訪問這個屬性的時候,都要在 origin 前面加上類名。 如同在例項屬性上使用 this.xxx 來訪問屬性一樣,這裡我們使用 Grid.xxx 來訪問靜態屬性。

/* 
靜態屬性, 是類物件的屬性
非靜態屬性, 是類的例項物件的屬性
*/

class Person {
  name1: string = 'A'
  static name2: string = 'B'
}

console.log(Person.name2)
console.log(new Person().name1)

抽象類

抽象類做為其它派生類的基類使用。 它們不能被例項化。不同於介面,抽象類可以包含成員的實現細節。 abstract 關鍵字是用於定義抽象類和在抽象類內部定義抽象方法。

/* 
抽象類
  不能建立例項物件, 只有實現類才能建立例項
  可以包含未實現的抽象方法
*/

abstract class Animal {

  abstract cry ()

  run () {
    console.log('run()')
  }
}

class Dog extends Animal {
  cry () {
    console.log(' Dog cry()')
  }
}

const dog = new Dog()
dog.cry()
dog.run()

2.4 函式

函式是 JavaScript 應用程式的基礎,它幫助你實現抽象層,模擬類,資訊隱藏和模組。在 TypeScript 裡,雖然已經支援類,名稱空間和模組,但函式仍然是主要的定義行為的地方。TypeScript 為 JavaScript 函式添加了額外的功能,讓我們可以更容易地使用。

基本示例

和 JavaScript 一樣,TypeScript 函式可以建立有名字的函式和匿名函式。你可以隨意選擇適合應用程式的方式,不論是定義一系列 API 函式還是隻使用一次的函式。

通過下面的例子可以迅速回想起這兩種 JavaScript 中的函式:

// 命名函式
function add(x, y) {
  return x + y
}

// 匿名函式
let myAdd = function(x, y) { 
  return x + y;
}

函式型別

為函式定義型別

讓我們為上面那個函式新增型別:

function add(x: number, y: number): number {
  return x + y
}

let myAdd = function(x: number, y: number): number { 
  return x + y
}

我們可以給每個引數新增型別之後再為函式本身新增返回值型別。TypeScript 能夠根據返回語句自動推斷出返回值型別。

書寫完整函式型別

現在我們已經為函式指定了型別,下面讓我們寫出函式的完整型別。

let myAdd2: (x: number, y: number) => number = 
function(x: number, y: number): number {
  return x + y
}

可選引數和預設引數

TypeScript 裡的每個函式引數都是必須的。 這不是指不能傳遞 nullundefined 作為引數,而是說編譯器檢查使用者是否為每個引數都傳入了值。編譯器還會假設只有這些引數會被傳遞進函式。 簡短地說,傳遞給一個函式的引數個數必須與函式期望的引數個數一致。

JavaScript 裡,每個引數都是可選的,可傳可不傳。 沒傳參的時候,它的值就是 undefined。 在TypeScript 裡我們可以在引數名旁使用 ? 實現可選引數的功能。 比如,我們想讓 lastName 是可選的:

在 TypeScript 裡,我們也可以為引數提供一個預設值當用戶沒有傳遞這個引數或傳遞的值是 undefined 時。 它們叫做有預設初始化值的引數。 讓我們修改上例,把firstName 的預設值設定為 "A"

function buildName(firstName: string='A', lastName?: string): string {
  if (lastName) {
    return firstName + '-' + lastName
  } else {
    return firstName
  }
}

console.log(buildName('C', 'D'))
console.log(buildName('C'))
console.log(buildName())

剩餘引數

必要引數,預設引數和可選引數有個共同點:它們表示某一個引數。 有時,你想同時操作多個引數,或者你並不知道會有多少引數傳遞進來。 在 JavaScript 裡,你可以使用 arguments 來訪問所有傳入的引數。

在 TypeScript 裡,你可以把所有引數收集到一個變數裡:
剩餘引數會被當做個數不限的可選引數。 可以一個都沒有,同樣也可以有任意個。 編譯器建立引數陣列,名字是你在省略號( ...)後面給定的名字,你可以在函式體內使用這個陣列。

function info(x: string, ...args: string[]) {
  console.log(x, args)
}
info('abc', 'c', 'b', 'a')

函式過載

函式過載: 函式名相同, 而形參不同的多個函式
在JS中, 由於弱型別的特點和形參與實參可以不匹配, 是沒有函式過載這一說的
但在TS中, 與其它面向物件的語言(如Java)就存在此語法

/* 
函式過載: 函式名相同, 而形參不同的多個函式
需求: 我們有一個add函式,它可以接收2個string型別的引數進行拼接,也可以接收2個number型別的引數進行相加 
*/

// 過載函式宣告
function add (x: string, y: string): string
function add (x: number, y: number): number

// 定義函式實現
function add(x: string | number, y: string | number): string | number {
  // 在實現上我們要注意嚴格判斷兩個引數的型別是否相等,而不能簡單的寫一個 x + y
  if (typeof x === 'string' && typeof y === 'string') {
    return x + y
  } else if (typeof x === 'number' && typeof y === 'number') {
    return x + y
  }
}

console.log(add(1, 2))
console.log(add('a', 'b'))
// console.log(add(1, 'a')) // error

2.5 泛型

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

引入

下面建立一個函式, 實現功能: 根據指定的數量 count 和資料 value , 建立一個包含 countvalue 的陣列
不用泛型的話,這個函式可能是下面這樣:

function createArray(value: any, count: number): any[] {
  const arr: any[] = []
  for (let index = 0; index < count; index++) {
    arr.push(value)
  }
  return arr
}

const arr1 = createArray(11, 3)
const arr2 = createArray('aa', 3)
console.log(arr1[0].toFixed(), arr2[0].split(''))

使用函式泛型

function createArray2 <T> (value: T, count: number) {
  const arr: Array<T> = []
  for (let index = 0; index < count; index++) {
    arr.push(value)
  }
  return arr
}
const arr3 = createArray2<number>(11, 3)
console.log(arr3[0].toFixed())
// console.log(arr3[0].split('')) // error
const arr4 = createArray2<string>('aa', 3)
console.log(arr4[0].split(''))
// console.log(arr4[0].toFixed()) // error

多個泛型引數的函式

一個函式可以定義多個泛型引數

function swap <K, V> (a: K, b: V): [K, V] {
  return [a, b]
}
const result = swap<string, number>('abc', 123)
console.log(result[0].length, result[1].toFixed())

泛型介面

在定義介面時, 為介面中的屬性或方法定義泛型型別
在使用介面時, 再指定具體的泛型型別

interface IbaseCRUD <T> {
  data: T[]
  add: (t: T) => void
  getById: (id: number) => T
}

class User {
  id?: number; //id主鍵自增
  name: string; //姓名
  age: number; //年齡

  constructor (name, age) {
    this.name = name
    this.age = age
  }
}

class UserCRUD implements IbaseCRUD <User> {
  data: User[] = []
  
  add(user: User): void {
    user = {...user, id: Date.now()}
    this.data.push(user)
    console.log('儲存user', user.id)
  }

  getById(id: number): User {
    return this.data.find(item => item.id===id)
  }
}


const userCRUD = new UserCRUD()
userCRUD.add(new User('tom', 12))
userCRUD.add(new User('tom2', 13))
console.log(userCRUD.data)

泛型類

在定義類時, 為類中的屬性或方法定義泛型型別
在建立類的例項時, 再指定特定的泛型型別

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

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

let myGenericString = new GenericNumber<string>()
myGenericString.zeroValue = 'abc'
myGenericString.add = function(x, y) { 
  return x + y
}

console.log(myGenericString.add(myGenericString.zeroValue, 'test'))
console.log(myGenericNumber.add(myGenericNumber.zeroValue, 12))

泛型約束

如果我們直接對一個泛型引數取 length 屬性, 會報錯, 因為這個泛型根本就不知道它有這個屬性

// 沒有泛型約束
function fn <T>(x: T): void {
  // console.log(x.length)  // error
}

我們可以使用泛型約束來實現

interface Lengthwise {
  length: number;
}

// 指定泛型約束
function fn2 <T extends Lengthwise>(x: T): void {
  console.log(x.length)
}

我們需要傳入符合約束型別的值,必須包含必須 length 屬性:

fn2('abc')
// fn2(123) // error  number沒有length屬性

2.6 其它

宣告檔案

當使用第三方庫時,我們需要引用它的宣告檔案,才能獲得對應的程式碼補全、介面提示等功能

什麼是宣告語句

假如我們想使用第三方庫 jQuery,一種常見的方式是在 html 中通過 <script> 標籤引入 jQuery,然後就可以使用全域性變數 $jQuery 了。

但是在 ts 中,編譯器並不知道 $ 或 jQuery 是什麼東西

/* 
當使用第三方庫時,我們需要引用它的宣告檔案,才能獲得對應的程式碼補全、介面提示等功能。
宣告語句: 如果需要ts對新的語法進行檢查, 需要要載入了對應的型別說明程式碼
  declare var jQuery: (selector: string) => any;
宣告檔案: 把宣告語句放到一個單獨的檔案(jQuery.d.ts)中, ts會自動解析到專案中所有宣告檔案
下載宣告檔案: npm install @types/jquery --save-dev
*/

jQuery('#foo');
// ERROR: Cannot find name 'jQuery'.

這時,我們需要使用 declare var 來定義它的型別

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

jQuery('#foo');

declare var 並沒有真的定義一個變數,只是定義了全域性變數 jQuery 的型別,僅僅會用於編譯時的檢查,在編譯結果中會被刪除。它編譯結果是:

jQuery('#foo');

一般宣告檔案都會單獨寫成一個 xxx.d.ts 檔案

建立 01_jQuery.d.ts, 將宣告語句定義其中, TS編譯器會掃描並載入專案中所有的TS宣告檔案

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

很多的第三方庫都定義了對應的宣告檔案庫, 庫檔名一般為 @types/xxx, 可以在 https://www.npmjs.com/package/package 進行搜尋

有的第三庫在下載時就會自動下載對應的宣告檔案庫(比如: webpack),有的可能需要單獨下載(比如jQuery/react)

內建物件

JavaScript 中有很多內建物件,它們可以直接在 TypeScript 中當做定義好了的型別。

內建物件是指根據標準在全域性作用域(Global)上存在的物件。這裡的標準是指 ECMAScript 和其他環境(比如 DOM)的標準。

  1. ECMAScript 的內建物件

Boolean
Number
String
Date
RegExp
Error

/* 1. ECMAScript 的內建物件 */
let b: Boolean = new Boolean(1)
let n: Number = new Number(true)
let s: String = new String('abc')
let d: Date = new Date()
let r: RegExp = /^1/
let e: Error = new Error('error message')
b = true
// let bb: boolean = new Boolean(2)  // error
  1. BOM 和 DOM 的內建物件

Window
Document
HTMLElement
DocumentFragment
Event
NodeList

const div: HTMLElement = document.getElementById('test')
const divs: NodeList = document.querySelectorAll('div')
document.addEventListener('click', (event: MouseEvent) => {
  console.dir(event.target)
})
const fragment: DocumentFragment = document.createDocumentFragment()