JavaScript相關的類型註解系統

TypeScrip

類型系統

強類型與弱類型(類型安全方面)

非權威機構的定義

強類型

  • 函數的實參與形參的類型必須相同
  • 類型約束
  • 不允許隱式類型轉換

弱類型

  • 函數的實參與形參的類型在語法上不必相同
  • 類型上約束少
  • 允許任意的數據隱式類型轉換
  • JavaScript的TypeError是在運行時通過邏輯判斷拋出的,而不是編譯時就會拋出。

靜態類型與動態類型(類型檢查方面)

靜態類型

  • 變量在聲明時類型就是明確的,且聲明後變量的類型不允許再修改
  • 通俗地說,靜態表示的是看一眼就知道是什麼

動態類型

  • 變量在運行時類型才能明確,且可以隨意更改變量的類型
  • 變量沒有類型,變量存放的值纔有類型
  • 通俗地說,動態表示只有運行的時候才知道是什麼

JavaScript自身類型系統的問題

JavaScript類型系統特徵

  • 弱類型 + 動態類型
  • 缺少類型系統的可靠性

弱類型的問題

  • 類型異常的情況只有運行代碼的時候才知道,導致不能及時發現問題
  • 沒有類型約束導致功能出現問題,如函數返回值對於不同的參數類型是不同的。
  • 類型的隱式轉換出現一些意料之外的問題

強類型的優勢

  • 錯誤更早發現
  • 代碼具備智能提示,編碼更準確
  • 重構更可靠
  • 減少代碼中不必要的類型判斷

Flow靜態類型檢查方案

靜態類型檢查工具

  • 類型註解的方式標註變量的類型let a: number = 6; // : number即爲類型註解
  • 可以通過Babel或Facebook官方的模塊在生產環境中去除類型註解
  • 不要求所有的變量都進行類型註解,可以按需註解

Flow的原理

對代碼進行了編譯,使得在開發階段就可以使用一些擴展語法。

Flow的使用

  • flow是一個node包,需要在使用前安裝npm install flow-bin -D。這裏使用了開發依賴來安裝flow-bin包,爲的是讓flow跟隨項目,讓項目的其他開發人員瞭解需要使用flow來進行靜態類型檢查。也可以使用全局安裝的形式,使用命令行來啓動flow的檢查。
  • 使用npx flow init命令生成.flowconfig配置文件
  • 在需要檢查的JS文件開頭添加// @flow標記或者更爲複雜註解方式。
// index.js文件
// @flow
/**
* 這是另一種在開頭添加標記的方式
* @flow
**/

function sum(a: number, b: number) {
  return a + b;
}
sum(100, 200);
// 在執行flow的時候,下面的調用會報錯
sum('100', '200');
  • 執行npx flow來讀取配置文件,執行flow服務。第一次執行的時候有點慢,是因爲啓動了一個後臺服務,之後再執行flow時,就會很快的。

編譯移除類型註解

flow-remove-types包

官方的移除註解方案

  • 安裝npm install flow-remove-types -D
  • 運行npx flow-remove-types 目標文件路徑 -d 生成文件路徑
Babel + @babel/preset-flow插件
  • 安裝npm install @babel/core @babel/cli @babel/preset-flow -D
  • 添加Babel配置文件.babelrc
// .babelrc文件
{
  "presets": ["@babel/preset-flow"]
}
  • 使用babel 目標文件路徑 -d 生成文件路徑啓動編譯
    以上的Flow靜態檢查都會把檢查結果輸入到控制檯,不太方便。

Flow Language Support插件(VSCode)

安裝vscode的Flow Language Support插件,可以在vscode開發過程中進行靜態類型檢查,將代碼中的類型問題以紅色波浪線顯示出來。
Flow官網給出的所有編輯器下支持的插件,點擊查看

Flow的類型推斷

Flow也可以根據值的類型推斷出存儲該值的變量的類型,從而顯示出類型問題。但儘量還是添加類型註解,增加代碼可讀性。

Flow中的類型註解方法

原始類型值
  • string:const a: string = 'foo';
  • number:const b: number = NaN; // NaN屬性number類型
  • boolean:const c: boolean = false;
  • null:const d: null = null;
  • undefined:const e: void = undefined; // 要標註值爲undefined類型時,需要用void
  • symbol:const f: symbol = Symbol('symbol type');
數組類型
  • const arr1: Array<number> = [1, 2, 3]; // 表示數組元素全部都是number類型
  • const arr2: number[] = [1, 2, 3];
  • const foo: [string, number] = ['foo', 6]; // 表示固定長度的數組,並指定元素類型,這裏實際上表示了元組這種類型
對象類型
  • const obj1: { foo: string, bar: number } = { foo: 'foo', bar: 6 } //對屬性值進行類型註解
  • const obj21: { foo?: string, bar: number } = { bar: 6 } // 可以在屬性名後添加?表示該屬性屬於可選屬性
  • const obj3: { [string]: string } = {}; obj3.key1 = 'value1'; obj3.key2 = 68; // 動態添加屬性的情況下,可以先指定屬性的類型
函數類型:對參數與返回值進行類型約束
  • 函數參數與返回值的類型註解
function square(n: number): number {
  return n * n;
}
// 沒有返回值則標記爲void
function foo(): void {
  console.log('void function');
}
  • 對函數作爲值的情況下進行類型約束,使用箭頭函數的方式來註解:(參數類型註解) => 返回值類型註解
function foo(callback: (string, number) => void) {
  callback('string', 66);
}
特殊類型
字面量類型

類型註解爲某個字面量值const a: 'foo' = 'foo'; // 要求變量a只能存放值'foo'; 一般這樣做意義不大,主要是配合聯合類型來使用

聯合類型
  • 聯合類型用來約束變量只能存放約束範圍內的值const type: 'success' | 'waring' | 'danger' = 'success'; // type變量只能存放'success'、'warning'、'danger'這三個值中的某一個
  • 聯合類型也可以約束變量只能存放約束範圍內的類型const b: string | number = 'foo'; // 變量b只能存放string類型或者number類型
MayBe類型

當變量可以是約束的類型,也可以是null或undefined時,使用?作爲類型前綴來註解。

// ?number表示除了number類型外,可以是undefined或null
const gender: ?number = undefined;
mixed類型

所有類型的聯合類型const a: mixed = 'foo';
mixed類型的意義在於變量不能在使用過程中隨意更改自己的類型。一旦確定了是哪種類型,之後就必須是該類型。換句話說,mixed依然是強類型。

any類型

任意類型const b: any = 'foo';
any類型允許變量在使用過程中隨意更改自己的類型,即弱類型。

類型別名(類型聲明)

使用type關鍵字來聲明一個類型(自定義一個類型,或者稱爲類型別名),然後可以使用該類型別名來做註解。

// type聲明一個StringOrNumber的類型別名,然後可以使用該類型別名來做註解
type StringOrNumber = string | number;
const c: StringOrNumber = 66;

Flow相關參考資料

官方類型手冊
第三方類型手冊,該手冊更加直觀清晰。

Flow運行環境API

如瀏覽器環境或Node環境下提供的API,這些API所對應的類型聲明文件。
這些API對應的類型聲明文件點擊這裏

TypeScript語言規範與基本應用

TypeScript是JavaScript的超集

超集 = JavaScript + 類型系統 + ES6+
TypeScript 通過編譯生成 JavaScript

缺點

  • 新的概念
  • 項目初期帶來一些成本

基本使用

  • 安裝:npm install typescript -D作爲開發依賴安裝,當然也可以全局安裝。局部安裝之後,就會出現/node_modules/bin/tsc文件,tsc作爲TypeScript編譯文件的命令。
  • 編譯:使用npx tsc 源文件路徑 -o 生成文件路徑來將源文件編譯爲JavaScript,將ES6+語法生成ES5甚至ES3的語法。
  • 配置:使用配置文件來爲編譯做配置

配置文件:爲編譯項目做統一配置

  • 生成配置文件:使用npx tsc --init在項目根目錄下生成tsconfig.json配置文件
  • 配置文件中的配置字段:“compilerOptions”
    • “target”:表示生成文件的ES語法
    • “module”:表示生成文件的模塊語法,默認爲CommonJS規範
    • “lib”:表示編譯時使用的類型聲明標準庫。如果不開啓時,默認與target字段相對應。即target如果是es5,則類型聲明庫使用es5的聲明庫,這樣一來源代碼中的ES6新增的API就會報錯。當lib設置爲[“es2015”]時,需要同時添加"DOM"聲明庫,即[“es2015”, “DOM”],因爲開啓了lib之後,屏蔽了默認使用的聲明庫,需要將瀏覽器環境下的API聲明庫再添加回來。
      • 標準庫:內置對象所對應的聲明文件
    • “outDir”:生成文件的路徑
    • “rootDir”:源文件的路徑
    • “sourceMap”:是否生成sourceMap
    • strict:是否使用嚴格模式類型檢查。嚴格模式要求爲每個變量明確指定類型,不允許類型推斷。
    • strictNullChecks:是否使用嚴格模式檢查null值
  • 只有在用tsc命令編譯項目時纔會讀取配置文件,如果只是編譯某個文件,則配置文件中的信息不會被讀取使用。
  • 編譯的時候回根據類型聲明文件(包括標準庫和自定義的類型聲明文件)進行編譯。

顯示中文錯誤消息

  • 使用npx tsc --locale zh-CN命令讓TypeScript在控制檯顯示中文錯誤消息。
  • 在vscode的settings中,找到TypeScript: Locale選項,設置爲zh-CN,這樣在vscode中顯示的錯誤消息就會是中文的。

作用域問題

如果文件之間不是模塊作用域或者函數作用域時,聲明的變量會被編譯到全局作用域,有可能會出現重複聲明的報錯問題。

TypeScript中的類型系統

TypeScript中註解原始類型值

基本與Flow一致。
不同點:

  • string、number和boolean在非嚴格模式下可以爲null
  • symbol類型:註解爲symbol時,當target不爲es2015時,值如果是Symbol類型會有報錯。
  • void類型:值可以是undefined或null(嚴格模式下不能爲null)

TypeScript中的引用類型約束

object類型

泛指所有的非原始類型,或者說object類型指的是所有引用類型。

const foo: object = {}; // 值可以是對象、數組、函數等等

如果單純指對象類型,則使用對象字面量來做類型註解或者接口interface。對象類型要求值與類型註解的shape完全一致。

const obj: { foo: number } = { foo: 66 }; // 這裏的值必須是屬性爲foo且值爲number類型的對象

數組類型

  • Array<T>Array泛型,如Array<string>
  • <T>[]泛型字面量,如number[]

元組類型

  • 元素數量和各個元素類型都明確的數組。
  • 使用字面量形式註解,如const tuple: [number, string] = [ 66, 'hello'];
  • 適合在一個函數中返回多個類型的值。如Map中的鍵值對
  • React中的Hook就使用了二元形式的元組類型

枚舉類型

  • 給一組值進行額外的信息標註(讓值有意義的描述)
  • 枚舉並不只是類型,TypeScript中的枚舉是有值的。
  • 有限的一組值。
  • 枚舉類型會在編譯時入侵源代碼,也就是說,枚舉類型會被加入到編譯後的生成代碼中,通過生成一個雙向鍵值對的方式(雙向鍵值對指的是通過鍵找到值,通過值找到鍵)
    使用enum關鍵字聲明一個枚舉類型的變量並初始化。
// 聲明枚舉類型PostStatus
enum PostStatus {
  // 注意這裏使用=而不是:來賦值
  Draft = 0,
  Unpublished = 1,
  Published = 2
}
// 默認情況下,值是從0開始的自增長,所以以上的聲明可以不寫值
enum PostStatus {
  Draft, Unpublished, Published
}
// 枚舉的值也可以是字符串,但這時候就需要明確的初始化了。
enum PostStatus {
  Draft = 'draft', Unpublished = 'unpublished', Published = 'published'
}
const post = {
  // 使用.符號來使用枚舉類型的值
  status: PostStatus.Draft
}

如果不需要通過索引來找到值或者鍵,則建議使用常量枚舉類型,在enum之前加入const。這樣枚舉類型代碼則不會入侵到源代碼中,生成的代碼只會使用枚舉的值。

const enum PostStatus {
  Draft, Unpublished, Published
}

函數類型

函數聲明約束
// b爲可選參數
function func1(a: number, b?: number, ...rest: number[]): string {
  return 'func1';
}
函數表達式約束
const func2: (a: number, b?number) => string = function(a: number, b?: number): string {
  return 'func2';
}

任意類型

使用any來註解類型,同樣也是弱類型。

聯合類型

  • 多種類型中的任意一個,使用 | 符號來連接多種類型
type Uni = string | number; // 定義類型別名Uni,string和number構成的聯合類型

交叉類型

  • 多種類型的合併爲一個類型,包含了所有類型的成員
type Uni = string & number; // 定義類型別名Uni, string和number構成的聯合類型

隱式類型推斷

根據代碼中變量的使用情況來對變量的類型做推斷。經過類型推斷的變量如果再重新賦予其他類型的值,就會報錯。
建議爲變量明確類型,而不是使用隱式推斷。

類型斷言

  • 明確告訴TypeScript編譯器某個變量的類型一定是某個類型,從而使得TypeScript不會因爲類型不唯一而出現不正確的報錯。
  • 使用as關鍵字來斷言,或者使用<T>來斷言(在JSX語法下這種方式會產生混淆,以爲<>)
  • 類型斷言不是類型轉換,類型斷言是編譯過程中的,類型轉換是運行時
const nums = [100, 101, 102];
const res = nums.find(i => i > 1); // 如果不使用斷言,這裏TypeScript會推斷res類型爲number | undefined,因爲TypeScript並不會執行代碼。實際上,這裏的res一定是number。
const square = res * res; // 如果不進行類型斷言,這裏的res會報錯,因爲undefined類型不可以這樣運算
const num1 = res as number; // as + 類型,斷言
const num2 = <number>res; // <T>斷言

接口Interface

  • 可以理解爲一種規範,約束對象的結構(成員及成員類型)
  • 使用interface關鍵字來聲明
  • 可選成員,在成員後添加?
  • 只讀成員,在成員之前添加readonly
  • 動態成員,如緩存對象在運行時會動態添加成員
  • 同名接口會自動合併(類則不可以)
interface Post {
  title: string;
  content: string;
  subtitle?: string; // 可選成員
  readonly summary: string;
}
// 緩存對象中動態成員
interface cache {
  // 使用[]來表示未命名的成員以及成員名的類型
  [prop: string]: string
}
function printPost(post: Post) {
  console.log(post.title, post.content);
}

描述一類具體對象的抽象成員
TypeScript增強了class的相關語法

基本使用

  • 類內的屬性成員需要先定義,不能動態添加;
class Person {
  // 需要先對成員進行類型註解
  name: string, // 可以在類成員定義時初始化,也可以在構造函數中初始化
  age: number
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
  sayHi(msg: string): void {
    console.log(`I am ${this.name}, ${msg}`);
  }
}

訪問修飾符

  • private:私有屬性,只有類內部可以訪問
  • protected:只有類和子類纔可以訪問
  • public:公共屬性,類外部也可以訪問
class Person {
  // 訪問修飾符對成員的訪問控制
  public name: string, // 可以在類成員定義時初始化,也可以在構造函數中初始化
  private age: number,
  protected gender: boolean
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
    this.gender = true;
  }
  sayHi(msg: string): void {
    console.log(`I am ${this.name}, ${msg}`);
  }
}

如果構造函數使用private修飾之後,則外部無法調用構造函數來創建實例。除非類暴露出靜態方法來供外部調用。

只讀屬性

使用readonly關鍵字,跟在訪問修飾符的後面,在成員名之前。

類與接口

接口是一種抽象的規範,類可以是抽象的規範,也可以是具體的實現。類可以使用implements關鍵字來表示對接口的實現。

// 定義一個接口,規範了該接口必須要有的成員,但成員是抽象的,不必具體實現
interface EatAndRun {
  eat(food: string): void;
  run(distance: number): void;
}
// 定義了一個Person類,該類具體實現了接口EatAndRun的規範
class Person implements EatAndRun {
  eat(food: string): void {
    console.log(`優雅地進餐: ${food}`);
  }
  run(distance: number): void {
    console.log(`直立行走: ${distance}`);
  }
}
// 定義了一個Animal類,該類也具體實現了接口EatAndRun的規範
class Animal implements EatAndRun {
  eat(food: string): void {
    console.log(`呼嚕呼嚕地喫: ${food}`);
  }
  run(distance: number): void {
    console.log(`爬行: ${distance}`);
  }
}

更合理的是,一個接口應該只規範一個成員

// 定義Eat接口
interface Eat {
  eat(food: string): void;
}
// 定義Run接口
interface Run {
  run(distance: number): void;
}
// 定義了一個Person類,該類具體實現了接口Eat、Run的規範
class Person implements Eat, Run {
  eat(food: string): void {
    console.log(`優雅地進餐: ${food}`);
  }
  run(distance: number): void {
    console.log(`直立行走: ${distance}`);
  }
}
// 定義了一個Animal類,該類也具體實現了接口Eat、Run的規範
class Animal implements Eat, Run {
  eat(food: string): void {
    console.log(`呼嚕呼嚕地喫: ${food}`);
  }
  run(distance: number): void {
    console.log(`爬行: ${distance}`);
  }
}
抽象類
  • 與接口相似,但可以有具體的實現。
  • 使用abstract關鍵字定義抽象類,抽象類只能繼承,不能用來構造實例。
  • 抽象類中也可以使用abstract關鍵字定義抽象方法,子類需要去具體實現抽象方法。

泛型

  • 指的是聲明時沒有指定具體類型,只有在使用的時候纔去具體指定類型。
  • 使用<>來指定具體類型,即類型斷言
// 創建一個長度爲length、元素爲value的數組
function createNumberArray(length: number, value: number): number[] {
  // const arr = Array(length).fill(value); 這種方式下使用Array(length)得到的數組元素是any類型,在Array標準庫聲明文件中,使用的是Array<T>泛型,指的是在使用Array()方法時,再指定<T>中T的類型。
  const arr = Array<number>(length).fill(value);
  return arr;
}

可以在聲明時使用泛型參數,等到調用時再指定泛型參數的類型。這樣可以更大程度複用代碼。

// 創建一個長度爲length、元素爲value的數組,這裏數組的元素是任意類型的,只有調用時傳入具體類型。
function createArray<T>(length: number, value: <T>): <T>[] {
  // const arr = Array(length).fill(value); 這種方式下使用Array(length)得到的數組元素是any類型,在Array標準庫聲明文件中,使用的是Array<T>泛型,指的是在使用Array()方法時,再指定<T>中T的類型。
  const arr = Array<T>(length).fill(value);
  return arr;
}
// 調用時指定泛型參數爲string
const arr = createArray<string>(3, 'hello');

類型聲明及類型聲明文件

  • 使用declare關鍵字來對用到的函數、類等進行類型聲明。一般是爲了對沒有類型聲明的第三方模塊進行補充聲明。
  • 很多第三方模塊在TypeScript社區都已經有類型聲明文件,只要安裝就可以了,注意是開發依賴。npm install @types/模塊名 -D。一般類型聲明文件是.d.ts後綴名文件,命名以@types/開頭。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章