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/開頭。