TypeScript基礎知識總結

一、什麼是TypeScript?

TypeScript是由微軟開發的一門腳本語言,它是JavaScript的一個超集,完全兼容JavaScript。也就是說,任何合法的JavaScript代碼,也都是合法的TypeScript代碼。

TypeScript以.ts結尾,經過編譯器編譯,可生成常規的.js代碼文件。如:
hello.ts

let msg: string = 'hello world';
function outputMsg (msg: string):void {
  console.log(msg);
}

outputMsg(msg);

這裏的msg: string是將變量msg聲明爲字符串類型,之後只要是給該變量賦值爲其他任何類型,TypeScript編譯器都將報錯。:void則是聲明該函數沒有返回值,如果該函數帶有返回值,編譯器也將報錯。

假設我們已經通過npm install -g typescript安裝了TypeScript,我們就可以在控制檯運行如下命令來生成對應的js代碼:

tsc hello.ts

現在hello.ts的相同目錄下會生成一個hello.js文件:

let msg = 'hello world';
function outputMsg (msg) {
  console.log(msg);
}

outputMsg(msg);

我們看到,TypeScript編譯器去掉了與變量或函數返回類型相關的類型聲明,生成了原生的js代碼,這個js代碼即可作爲靜態資源使用。

TypeScript出現的目的是解決JavaScript缺少靜態類型檢查,因此不適合開發大型項目的問題。它在JavaScript的基礎上新增了類型檢查、類型推斷、接口、泛型等一些屬於靜態語言的特性,然後通過TypeScript編譯器編譯爲純JavaScript代碼,以運行在各大平臺之上。

TypeScript被設計爲JavaScript的超集,這使得從JavaScript過渡到TypeScript的代價非常低。甚至即使你直接把js文件改名爲.ts後綴,也可以正常被TypeScript的編譯器所編譯。使用TypeScript幾乎已經是前端開發的必然趨勢,下面我們就來學習一下TypeScript的入門知識吧。

:本文只探討TypeScript的新增語法,JavaScript的基礎語法不在本文列舉。

二、TypeScript基礎

1. TypeScript的安裝

安裝TypeScript非常簡單,只需要運行如下命令:

npm install -g typescript

現在我們的全局npm目錄下就新增了TypeScript的編譯器包,它提供的tsc命令可以將一個.ts後綴的文件編譯爲js文件。如下命令可以編譯一個ts文件:

tsc hello.ts
// 生成hello.js

如果要在開發工具中使用TypeScript,只需要安裝對應的插件即可。

新版本的VS Code已經默認啓用了對TypeScript的支持,因此無需額外安裝插件。如果需要使用webpack對包含ts代碼的項目進行打包,可以配置對應的loader:ts-loader,它會在打包時自動將ts代碼轉換爲js代碼生成出來:
webpack.config.js

{
  test: /\.tsx?$/,
  loader: 'ts-loader',
  exclude: /node_modules/,
  options: {
    appendTsSuffixTo: [/\.vue$/],
  }
}

項目的根目錄下需要新增tsconfig.json,以配置TypeScript的行爲:

{
  "include": [
    "src/**/*"
  ],
  "exclude": [
    "node_modules"
  ],
  "compilerOptions": {
    "allowSyntheticDefaultImports": true,
    "experimentalDecorators": true,
    "allowJs": true,
    "module": "es2015",
    "target": "es5",
    "moduleResolution": "node",
    "experimentalDecorators": true,
    "isolatedModules": true,
    "lib": [
      "dom",
      "es5",
      "es2015.promise"
     ],
    "sourceMap": true,
    "pretty": true
  }
}

以上配置的含義在這裏不再詳述,可參考官網手冊tsconfig.json

2. 數據類型

TypeScript是一種強類型腳本語言,它要求程序應該明確指定變量或函數返回值的類型(即使這個類型是any)。TypeScript約定的類型如下:

數據類型 關鍵字 說明
任意類型 any 不限制變量類型,該關鍵字常用於兼容JavaScript代碼
數字類型 number 對應JavaScript的number
字符串類型 string 對應JavaScript的string
布爾類型 boolean 對應JavaScript的boolean
數組類型 - TypeScript的數組沒有特定關鍵字,它的聲明方式由每一項的類型決定。如數字類型的數組,類型爲number[];或以泛型表示爲:Array<number> 。TypeScript的數組元素只能是同一種數據類型。
元組 - 帶有不同數據類型的特定數組在TypeScript中稱爲元組,如let x: [string, number]
枚舉 enum 與靜態語言的枚舉類型一致
void void 空,表示函數沒有返回值
null null 空對象
undefined undefined 變量未初始化
never never 代指永遠不可能出現的值,常用於無法執行完畢或結束語句不可達的函數的返回類型

這裏我們只介紹幾個TypeScript中較爲特殊的幾個數據類型。

  1. any:表示該變量允許爲任意類型。如:
let a: any = 123;
a = '123';
a = {};  

爲變量賦值爲任意類型均不會報錯。在改造現有的JavaScript項目時,這種數據類型非常有用,因爲既有項目的某些變量可能無法保證只保存一種類型的數據。

  1. 數組:對應JavaScript的數組,但是要求每一項的數據類型必須一致。聲明數組主要有以下兩種方式:
// 一般語法
let arr: number[] = [1, 2, 3];

// 泛型語法
let array: Array<number> = [4, 5, 6];
  1. 元組:對上述類型的補充,它允許數組的數據類型不一致,但是必須與定義時指定的數據類型一致。如:
let author: [string, number] = ['carter', 25];

author = [25, 'carter'];  // 編譯報錯,與元組類型不一致
  1. 枚舉:定義一組常量,常用於消除魔術字符串。如:
enum Color {Red, Green};
let c: Color = Color.Green;
console.log(c);    // 輸出 1

enum Color {Red, Green, Blue}被編譯後會生成如下代碼:

var Color;
(function (Color) {
    Color[Color["Red"] = 0] = "Red";
    Color[Color["Green"] = 1] = "Green";
})(Color || (Color = {}));
;

它大致是這樣一個對象:

var Color = {
  "0": "Red",
  "Red": "Red",
  "1": "Green",
  "Green": "Green",
}
  1. void:表示函數無返回值。熟悉java或者c等靜態語言的應該經常見到了:
function output():void {
  console.log('123');
}
  1. never:表示函數不會結束或函數的結束語句不可達。如:
function error():never {
  throw new Error('error msg');
}

function doWhile():never {
  while(1){ ... }
}

上述兩個函數都無法正常結束,因此它們的返回值是never。

如果一個變量可能存在多種變量類型,並且這些類型是已知的,那麼可以通過聯合類型羅列出所有可能使用的類型,如:

function f(arg: number | string):void {
  ...
}

f(123);
f('123');

3. 類型斷言(Type Assertion)

類型斷言的目的是手動將一個變量指定爲某種數據類型。爲什麼需要手動指定呢?

這是因爲TypeScript需要對代碼進行靜態分析,當變量可能擁有多種數據類型時,對TypeScript來說,只有這些數據類型都支持的操作纔是安全的操作,如:

function f(v: string | number):number {
  if (v.length) {
    ...
  } else {
    ...
  }
}

該函數接收一個參數,類型可爲字符串或數字。假設傳入的參數是數字,那麼length將是不存在的,函數執行就會報錯。爲了防止這種情況發生,TypeScript只能拋出錯誤,告知string | number這種聯合類型的變量不能訪問length屬性。

然而如果在某些情況下你明確知道變量的類型,則可以通過類型斷言的方式告知TypeScript編譯器,如:

function f(v: string | number):number {
  if ((<string>v).length) {
  // 或if ((v as string).length) {
    ...
  } else {
    ...
  }
}

<string>vv as string這兩種語法都可以將變量v的數據類型指定爲字符串,這樣在訪問length屬性時,編譯器就不會報錯了。

這種語法看上去很像java中的強制類型轉換,但其實它們不是一回事。java中的強制類型轉換存在一個轉換過程,涉及到某些編譯操作;而TypeScript的類型斷言並未進行任何類型轉換,它只是根據斷言結果啓用了更加明確的編譯規則。

類型斷言使得變量也可以跨類型賦值,如:

var str = '1' 
var str2: number = <number> <any> str;
var str3: number = str2;

我們看到,str是字符串,而str2是數值類型,按理說無法直接賦值。但是我們通過<number> <any> str的語法,先是將str斷言成any類型,相當於暫時消除了str原本的數據類型,然後又從any斷言爲number,這樣它就可以被賦值給str了。賦值後的變量str2仍然是數值 類型,因此它可以被賦值給數值類型的str3。

這裏我們必須先把字符串斷言爲any,才能再斷言爲number,因爲number和string之間不允許直接進行類型轉換斷言。

通常來說,跨類型的變量賦值在TypeScript中是不允許的,類型斷言給了我們權限來跳過這項規則。

如果在編寫TypeScript代碼時沒有明確指定變量類型,那麼TypeScript會根據初始化的值來推測數據類型,並以此作爲依據進行後續的類型推斷:
demo.ts

let a = 123;
a = '123';  // 這在TypeScript編譯時會報錯

由於a被賦值爲123,因此它的數據類型被推斷爲數值,後續爲其賦值字符串時就會看到一條類型推斷的報錯。不過即使有報錯,對應的js代碼依然可以生成(可以通過命令參數阻止TypeScript在有編譯錯誤時生成js代碼)。

4. 函數

TypeScript中函數的主要變化體現在參數和返回值上。

關於返回值,上文已經提及,可以用以下語法定義返回值類型:

function f(): number {
  return 123;
}

關於參數,有幾點不同。

首先,TypeScript指定的參數是必傳的,而且數據類型必須匹配,除非使用?標記爲可選參數。語法如下:

function buildName(firstName: string, lastName?: string) {
  if (lastName)
    return firstName + " " + lastName;
  else
    return firstName;
}

這裏firstName必傳,不傳會報錯。而lastName是可選的,因爲它帶有?

其次,函數參數也可以指定默認值,但指定了默認值的參數不允許設爲可選:

function cal(price:number,rate:number = 0.50) { 
    var discount = price * rate; 
    console.log("計算結果: ",discount); 
} 

這是因爲設置了默認值的參數必定是有值的,因此它不可能是可選參數。

最後,所有的可選參數必須位於參數列表的後面。這是因爲函數參數在賦值時是按序的,寫在前面的參數總會先被賦值,因此可能不存在的變量一定位於後面。

5. 接口(interface)

接口定義了一個具有特定結構的對象原型。它以抽象的形式描述了某類對象應該具有的屬性和方法,任何繼承了該接口的對象都必須實現接口所描述的所有的屬性和方法。舉例如下:

interface IPerson { 
    firstName:string, 
    lastName:string, 
    sayHi: ()=>string 
} 
 
var customer:IPerson = { 
    firstName:"Tom",
    lastName:"Hanks", 
    sayHi: ():string =>{return "Hi there"} 
} 

接口IPerson描述了一類數據結構,它具有屬性firstNamelastName和方法sayHi,但是沒有指定它們的值。

現在我們定義了一個變量customer,customer:IPerson表明它實現了IPerson接口,所以它需要實現接口所指定的屬性和方法(否則編譯會報錯)。

在TypeScript中,實現某個接口的對象,必須具有和接口完全一致的屬性和方法,不可以新增任何其他屬性或方法。如果需要這樣做,通過繼承來實現:

interface IParent {
  name: string,
  age: number,
}

interface IChildren extends IParent {
  sayHi: ():string => { return 'Hi'; }
}

一個接口可以同時繼承多個接口,這稱爲多繼承,它將同時獲得這些接口所定義的屬性和方法:

interface IParent1 { 
    v1:number 
} 
 
interface IParent2 { 
    v2:number 
} 
 
interface Child extends IParent1, IParent2 { ... } 

6. 類(class)

實際上ES6的語法中已經引入了類的概念,儘管我們知道,JavaScript的類的實現與java是不一樣的,但是基本語法卻有很多相似之處。

一個類可以包含以下三類成員:

  1. 字段。也就是屬性,包括實例屬性和靜態屬性。
  2. 構造函數。構造this所用的方法。
  3. 方法。定義對象支持的操作。

一個簡單的類如下:

class Car { 
    // 字段 
    engine:string; 
    static className:string, // 靜態屬性
    // 構造函數 
    constructor(engine:string) { 
        this.engine = engine 
    }  
    // 方法 
    disp():void { 
        console.log("發動機爲 :   "+this.engine) 
    } 
}

let car = new Car('v8');

除了特殊的類型聲明外,這與es6的class沒有太大差別。繼承的語法也是類似的,使用extends關鍵字:

class Benz extends Car {
  ...
}

當然了,像java中一樣,類可以用於實現一個接口,也使用implements關鍵字。實現接口時不要求類的屬性和方法和接口保持一致,只要求類必須實現接口所定義的全部屬性和方法即可:

interface ILoan { 
   interest:number 
} 
 
class AgriLoan implements ILoan { 
   interest:number 
   rebate:number 
   
   constructor(interest:number,rebate:number) { 
      this.interest = interest 
      this.rebate = rebate 
   } 
} 

TypeScript還參照java的類,爲JavaScript引入了訪問控制符,即publicprotectedprivate。其中public是默認權限,沒有指定類型的屬性或方法均爲public類型。如:

class Encapsulate { 
   str1:string = "hello" // public類型
   private str2:string = "world" 
}
 
var obj = new Encapsulate() 
console.log(obj.str1)     // 可訪問 
console.log(obj.str2)   // 編譯錯誤, str2 是私有的

7. 對象

TypeScript中定義對象的方法和JavaScript是一致的,但是它不允許爲一個已定義的對象直接新增屬性或方法:

let obj = { name: '張三' };

obj.name = '李四';  
obj.age = 25; // 報錯,obj沒有age屬性

obj在初始時只有name屬性,在嘗試爲其新增age屬性時就會報錯。如果想要設置它的age屬性,應該設置一個初始值來佔位:

let obj = { name: '張三', age: -1 };

鴨子類型(Duck Typing)

"當看到一隻鳥走起來像鴨子、游泳起來像鴨子、叫起來也像鴨子,
那麼這隻鳥就可以被稱爲鴨子。"

比如我們定義瞭如下接口:

interface IPoint {
  x: number,
  y: number
}

function addPoints(p1:IPoint,p2:IPoint):IPoint { 
    var x = p1.x + p2.x 
    var y = p1.y + p2.y 
    return {x:x,y:y} 
} 
// 正確
var newPoint = addPoints({x:3,y:4},{x:5,y:1})  
// 錯誤 
var newPoint2 = addPoints({x:1},{x:4,y:3})

在這裏,我們傳入addPoints的參數並未明確指定爲IPoint類型。但是對於TypeScript來說,只要該對象具備number類型的x屬性,和number類型的y屬性,那麼它就可以作爲IPoint類型的數據傳入函數。

8. 命名空間(namespace)

所謂的命名空間,就是一個相對獨立的命名作用域。這個作用域內所定義的變量,與外部的任何變量互不干擾,因此可以解決變量重名問題。

舉個例子,假設某學校一年級5班和6班都有一個學生叫“王小明”,在不指定班級的情況下,我麼無法知道“王小明”到底指哪個人。現在如果我們帶了班級前綴,指明是6班的“王小明”,那麼兩個人就得以區分了。這裏的5班和6班,就是我們所說的兩個命名空間。

TypeScript使用namespace來定義一個命名空間:
shape.ts

namespace Shape { 
   export interface IShape {      }  
   export class Rect {      }  
}

namespace Shape2 {
  export interface IShape {      }  
  export class Triangle {      }  
}

我們在同一個文件內定義了兩個命名空間:Shape和Shape2,這就好比是我們上面所說的5班和6班。這兩個空間內都有名爲IShape的接口,它就像我們上面所說的“王小明”同學。

由於命名空間的存在,我們可以很容易區分這兩個接口,方法是在使用的時候加上命名空間前綴,如Shape.IShapeShape2.IShape(這就像在稱呼“5班的王小明”和“6班的王小明”)。需要注意的是,命名空間內的變量、接口或類必須通過export向外暴露才可以被訪問,否則它就只在該命名空間內部可訪問。

當然了,在同一個文件裏聲明兩個命名空間並不常見,更多的情況是,將某個ts文件聲明爲一整個命名空間。不同的ts文件可以聲明到同一個命名空間下,這樣它們的變量在訪問時就不需要加命名空間前綴,但是由於被聲明到了同一命名空間,此時變量就不能重名了。

TypeScript使用如下的三斜線語法引入其他ts文件,這種語法相當於把被依賴的文件直接複製到當前文件:
Color.ts

/// <reference path="Shape.ts"/>
namespace Color {
  // Rect是Shape命名空間的類,需要加命名空間前綴才能調用
  let rect = new Shape.Rect();
  ...
}

9. 模塊

TypeScript的模塊系統與CommonJS、AMD以及ES6的模塊系統沒有明顯的差異,也是使用import、export和require這幾個關鍵字進行模塊的導入和導出。

TypeScript的模塊語法更像是CommonJS和ES6模塊系統的糅合產物,它使用如下的export語法向外暴露接口(這是借鑑自ES6模塊語法):
IShape.ts

export interface IShape { 
   draw(); 
}

然後在另一個文件中通過以下語法引入上述接口:
Circle.ts

import shape = require('./IShape.ts');
export class Circle implements shape.IShape { 
   public draw() { 
      console.log("Cirlce is drawn (external module)"); 
   } 
}

這裏的導入語法類似於ES6模塊語法和CommonJS的混合語法:

// ES6語法
import IShape from './IShape.js';
// CommonJS語法
let IShape = require('./IShape.js');

TypeScript相當於隱式地將模塊導出爲一個對象,然後將通過export暴露的變量、方法、接口、類等作爲屬性添加到該對象上,在其他文件中通過import導入的就是這個對象本身。

10. 聲明文件

TypeScript的聲明文件,類似於C/C++的頭文件,主要用於靜態類型檢查。

在大多數項目中,我們不免會引入一些第三方JavaScript庫,如jQuery。但是這些庫可能不一定是用TypeScript編寫的,所以當引入這樣的庫時,TypeScript就無法直接對其進行類型檢查,甚至在進行類型檢查時還可能產生編譯報錯。如:

let btn = $("#btn");  // 報錯,因爲當前文件並未定義$

上面的代碼會產生編譯報錯,因爲TypeScript在編譯上述文件時根本不知道$的數據類型是什麼,甚至不知道它是否存在。

引用一個用TypeScript重新實現的jQuery的確可以解決上述問題,但第三方JavaScript庫的數量至少數以百萬計,每一個庫都可能遇到這個問題,對其逐一改造實在不現實。爲此,TypeScript提供了聲明文件來解決這個問題。

聲明文件以.d.ts結尾,僅用於聲明某些沒有指明類型的變量、函數、類等,比如上面的jQuery的例子:
jquery.d.ts

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

我們在聲明文件裏通過declare關鍵字手動定義了變量$jQuery的類型,它們都是接收一個字符串作爲參數,返回值爲任意類型的函數。我們在自己的ts文件中直接引入上述聲明文件:

/// <refrence path="./jquery.d.ts"/>
let btn = $('#btn');

現在編譯就不會報錯了,因爲我們已經在聲明文件裏告訴TypeScript編譯器,$jQuery是一個接收一個字符串參數的函數了,TypeScript檢查發現我們的調用符合該規則時就認爲該語句沒有問題,從而編譯通過。

主流的第三方JavaScript庫都有現成的聲明文件,我們無需手動編寫,如jquery的聲明文件,運行npm install @types/jquery即可安裝。凡是由declare聲明的變量、函數等,在編譯完後都會被去除,它們僅用於幫助TypeScript識別未指明數據類型的標識符。

總結

TypeScript的設計初衷是爲JavaScript添加靜態語言的語法特性,使之能適應大型項目的開發。從目前的形勢來看,前端開發者學習TypeScript已是勢在必行。

與TypeScript類似的還有Facebook的Flow,但目前已經處於瀕死狀態(畢竟連Facebook自己的項目都選擇用TypeScript,而不是自家的Flow;Vue 2.x也是採用Flow,後來尤大神也在知乎承認是自己當初看走了眼,因此3.0版本已經完全改爲TypeScript實現),已經沒有學習的必要。

本文只是TypeScript的一些基礎語法,關於TypeScript在項目中的使用,還需要更多的經驗積累,以後再行補充。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章