學習TypeScript

本文同步在我的博客shymean.com上,歡迎關注javascript

最近用TypeScript寫了一個遊戲,順道把博客的後端項目也改爲了ts版本,下面整理在學習TypeScript時碰見的的問題。css

本文主要整理了ts的相關語法,以及一些概念,建議直接閱讀官方文檔,參考html

開發環境

安裝

首先全局安裝tsc前端

npm install -g typescript
複製代碼

而後就能夠編寫typescript文件並將其編譯成javascript文件並執行了vue

# 建立test.ts文件並寫入內容
touch test.ts && echo 'let a:number = 100; console.log(a);' > test.ts
# 編譯ts文件
tsc test.ts
# 在當前目錄下生成test.js
node test.js
複製代碼

vscode對typescript的支持十分友好(畢竟vscode就是ts寫的),所以建議使用vscode或webStorm做爲ts開發工具。java

若是每次修改ts文件都須要編譯一次再運行,則顯得比較繁瑣,可使用ts-node這個工具直接運行ts代碼,這在編寫demo代碼時很是有效node

npm install -g ts-node
# 直接執行ts文件
ts-node test.ts
複製代碼

tsconfig

除了上面指定tsc filename.ts的基礎調用方式以外,還能夠配置額外的命令行參數,如輸入文件、輸出目錄等react

tsc index.ts --相關參數
複製代碼

將編譯配置參數都經過命令行的形式傳入是一件比較麻煩的事,所以ts提供了一個叫tsconfig.json的配置文件,用來指定ts編譯的一些參數信息,包括用來編譯這個項目的根文件和編譯選項。webpack

若是一個目錄下包含tsconfig.json文件,那麼該目錄將會做爲這個ts項目的根目錄git

具體配置參數能夠參考

tsconfig並非項目必須的,初學時能夠直接跳過。瞭解了開發環境的搭建周後,接下來學習TypeScript的基礎語法。

變量類型

TypeScript裏的類型註解是一種輕量級的爲函數或變量添加約束的方式。其格式爲

variableName: variableType
複製代碼

首先須要明確的是,ts中存在兩種聲明空間:類型聲明空間與變量聲明空間

  • 類型聲明空間包含用來當作類型註解的內容,如 class XXXinterface XXXtype XXX
  • 變量聲明空間包含可用做變量的內容,如let iconst j

下面整理了ts中的變量類型,包括基礎類型和可自定義類型的一些寫法,建議直接閱讀官方文檔

基礎類型

原始類型

TS在類型聲明空間中,內置了一些基礎的數據類型

  • boolean,布爾值

  • number,ts全部數字都是浮點數

  • string,與js相同,支持模板字符串

  • 數組:

    • 基礎類型[],例如number[]
    • 數組泛型,例如Array<number>
  • 元組[string, number],須要保證對應位置的元素類型保持一致

  • enum

  • enum Color {Red = 1, Green = 2, Blue = 4}
      let c: Color = Color.Green;
    複製代碼
  • any,主要用於在編程階段還不清楚類型的變量指定一個類型,使用any能夠直接讓這些變量經過編譯階段的檢查

  • void,與any相反,表示沒有任何類型,一般聲明函數沒有任何返回值

  • null和undefined,在--strictNullChecks模式下,nullundefined只能賦值給void和它們各自

  • never,表示的是那些永不存在的值的類型

非原始類型

除了上面的基本類型以外的全部類型,都被稱爲object,表示非原始類型。

在某些時候,咱們須要主動肯定某個變量的類型,此時能夠經過類型斷言告訴ts編譯器

let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;

let strLength2: number = (someValue as string).length;
複製代碼

類型斷言能夠理解爲強制類型轉換。

接口

TypeScript的核心原則之一是對值所具備的結構進行類型檢查,接口的做用就是爲這些類型命名和爲你的代碼或第三方代碼定義契約。

聲明變量的類型

傳入的對象參數實際上會包含不少屬性,可是編譯器只會檢查那些必需的屬性是否存在,而且其類型是否匹配,可使用interface接口來自定義變量的類型

// interface定義接口
interface Person {
    name: string; //定義屬性名name,對應數據類型爲string,不包含該屬性會報錯
    age?: number; // 可選屬性,若是不傳也不會報錯
    readonly from: string; // 只讀屬性
    [prop: string]: any; // 容許包含其餘數據類型爲any屬性,去掉則沒法傳遞avatar參數
}
// 指定了greet函數的參數類型爲Person
function greet(p: Person):void {
    console.log(p.name);
    // p.from = '123'; 沒法修改只讀屬性
	  console.log(p.xxx); // 由於聲明瞭[prop: string]: any,致使此處不會報錯

    if (p.age) {
        console.log(p.age);
    }
}
// 此處就會對參數類型進行檢測
greet({ name: "shymean", from:"chengdu", age: 18, avatar: "http://xxx/xx.jpg" });
複製代碼

類型檢測限制了變量的類型,而可選屬性的好處有

  • 能夠對可能存在的屬性進行預約義,放寬了對於變量的屬性檢測限制
  • 相比較使用[prop: string]: any,能夠捕獲引用了不存在的屬性時的錯誤

聲明函數的簽名

函數的簽名包括了參數和返回值類型,因爲JavaScript中函數能夠經過函數表達式進行聲明,所以在ts中,接口除了描述自定義變量的類型,也能夠用來描述函數的類型

interface greetFunc {
  (person: Person): void;
}
let greet: greetFunc = function greet(p: Person) {};
// 調用方式同上
greet({ name: "shymean", from:"chengdu", age: 18, avatar: "http://xxx/xx.jpg" });
複製代碼

類的接口

接口也能夠用來限定某個類的實現

interface ClockInterface {
    currentTime: Date;
  	showTime(time: Date):void
}
// 類須要實現接口的屬性和方法
class Clock implements ClockInterface {
    currentTime: Date; // 若是不聲明ClockInterface接口上的屬性,則會提示錯誤
    constructor(h: number, m: number) { }
    showTime(){}
}
複製代碼

須要注意的是:當一個類實現了一個接口時,只對其實例部分進行類型檢查,類的靜態部分再也不檢查範圍內。

其餘

接口也能夠互相繼承,組合成新的接口,這樣能夠在多個接口間複用公共的屬性

class是一種比較常見的自定義類型,注意class Person除了在類型聲明空間提供了一個Person的類型,也在變量聲明空間提供了一個可使用的變量Person用於實例化對象

下面是一個簡單的例子

class Person {
    // 增長訪問修飾符
    private age: number; // 只能在當前類中訪問
    protected name: string; // 只能在當前類及其子類訪問
    gender: number; // 默認public,可在實例上訪問

    constructor(name: string, age: number, gender: number) {
        this.name = name;
        this.age = age;
        this.gender = gender;
    }

    // 方法也可使用訪問修飾符
    public show() {
        console.log(`${this.name}:${this.age} ${this.gender}`);
    }
}

let p: Person = new Person("shymean", 10, 1);
console.log(p.gender);
// console.log(p.name); // 訪問protected屬性會報錯
// console.log(p.age); // 訪問private屬性會報錯
p.show();

// 繼承
class Student extends Person {
    static count: number = 0;
    readonly grade: string = undefined; // 只讀屬性
    constructor(name: string, age: number, gender: number, grade: string) {
        // 構造函數裏訪問 this的屬性以前,必定先要調用 super()
        super(name, age, gender);
        this.grade = grade // 只讀屬性只能在聲明時或者構造函數中初始化

        // 靜態屬性
        Student.count++;
    }
  	// 重寫父類的show方法
    show(){
        // 能夠訪問父類的public和protected屬性
        console.log(`${this.name} ${this.gender}`);
        // 沒法訪問父類的私有屬性
        // console.log(this.age);
        
        // 調用父類方法
        super.show(); 
    }
    // 靜態方法
    static getCount() {
        // this.show() // 靜態方法類沒法調用實例方法
        console.log(Student.count)
    }
}

let s = new Student("shymean", 10, 1, "freshman");
s.show()

let s2 = new Student("shymean2", 10, 1, "freshman");

Student.getCount() // 返回 2

function test(p:Person){
    p.show()
}

test(p);
test(s); // 子類也能夠經過類型檢測
複製代碼

類型別名

類型別名會給一個類型起個新名字。 類型別名有時和接口很像,可是能夠做用於原始值,聯合類型,元組以及其它任何你須要手寫的類型

type User = {
    name: string,
    age: number
}
type Name = string;
複製代碼

類型別名與接口的區別在於

  • 接口建立了一個新的名字,能夠在其它任何地方使用。 類型別名並不建立新名字—好比,錯誤信息就不會使用別名
  • 類型別名不能像接口同樣被繼承和實現

代碼複用

泛型

爲了擴展函數的可複用性,接口不只要可以支持當前的數據類型,也須要可以支持將來的數據類型,這就是泛型的概念。也就是說,咱們能夠在編寫代碼時(如調用方法、實例化對象)指定數據類型。

泛型變量

一個典型的例子是:函數須要返回與參數類型相同的值

// T能夠看作是正則表達式裏面的捕獲,獲取了參數的類型後,就能夠用來聲明返回值的類型了
function identity<T>(arg: T): T {
    return arg;
}
let a: number = identity<number>(2)
let b: string = identity<number>(2) // 報錯:沒法將number類型賦值給string類型
let c: string = identity<string>("hello"); // 傳入不一樣的T類型
複製代碼

泛型接口

在上面的例子中,能夠經過泛型接口來指定泛型類型

interface identityFunc {
    <T>(arg: T): T
}

let identity: identityFunc = function<T>(arg: T): T {
    return arg;
};
複製代碼

泛型類

泛型類看上去與泛型接口差很少。 泛型類使用( <>)括起泛型類型,跟在類名後面。

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

// 實現一個數字的add方法
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) {
    return x + y;
};
let a: number = myGenericNumber.add(10, 20);

// 實現一個字符串的add方法
let myGenericString = new GenericNumber<string>();
myGenericString.zeroValue = '';
myGenericString.add = function(x, y) {
    return x + y;
};
let d: string = myGenericString.add("hello", "world");
複製代碼

泛型約束

在某些時候須要指定實現某些特定的數據類型

interface Lengthwise {
    length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);  
    return arg;
}
loggingIdentity(1) // T須要實現 Lengthwise,即包含length屬性
複製代碼

模塊

模塊是自聲明的;兩個模塊之間的關係是經過在文件級別上使用imports和exports創建的。

當目錄下包含了tsconfig.json文件後,該項目就成了一個ts項目

任何包含頂級import或者export的文件都被當成一個模塊,模塊在其自身的做用域裏執行,而不是在全局做用域裏。相反地,若是一個文件不帶有頂級的import或者export聲明,那麼它的內容被視爲當前ts項目中全局可見的(所以對模塊也是可見的)。

經過export關鍵字來導出變量,函數,類,類型別名或接口

// mod1.ts
export interface Person {
  	name: string
}
export const numberRegexp = /^[0-9]+$/;

let count = 100;
export { count as defaultCount }; // 導出重命名

// 默認導出
export default {
    test():void{
        console.log('mod1 test')
    }
}
複製代碼

經過import關鍵字來導入模塊的部分或所有內容

import {Person} from './mod1'
import { numberRegexp as re } from "./mod1";
import * as mod1 from "./mod1";
import mod1Util from "./mod1"; // 引入默認導出的內容,不須要與模塊中的聲明同名

// 使用mod1模塊中的內容
let p: Person = {name:'shyean'}
console.log(re.test('123'));
console.log(mod1.defaultCount);
mod1Util.test();
複製代碼

爲了兼容CommonJS和AMD的環境裏的exports變量,ts還支持export =方式

// mod3.ts
export = {
    test():void{
        console.log('mod3 test')
    }
}
複製代碼

上面這種形式導出的模塊,須要經過import = require()引入

import mod2 = require('./mod3')
mod2.test()
複製代碼

js支持多種模塊語法,如CommonJS、AMD、CMD、ES6模塊等,在將ts編譯爲js時,能夠經過指定--module moduleName的形式將代碼編譯爲指定模塊形式的代碼

tsc --module commonjs Test.ts
複製代碼

編譯完成後,每一個模塊文件都會生成一個單獨的.js文件

從JavaScript項目遷移

*.d.ts聲明文件

因爲ts增長了類型聲明,使用變量前須要先進行聲明,這致使在調用不少原生接口(瀏覽器、Node.js)或者第三方模塊的時候,由於變量未聲明而致使編譯器的類型檢查失敗。

因爲目前主流的庫都是經過JavaScript編寫的,且用 ts 寫的模塊在發佈的時候仍然是用 js 發佈,所以手動修改原生接口或者第三方模塊源碼確定是不現實的,如何對原有的JS庫也提供類型推斷呢?

typescript前後提出了 tsd(已廢棄)、typings(已廢棄)等功能,最後提出了 DefinitelyTyped,該規範須要用戶編寫.d.ts類型定義文件,用來給編譯器以及IDE識別是否符合API定義類型。

下面是一個描述d.ts文件做用的例子。假設有一個以前編寫的js工具庫util.js

// util.js
module.exports = {
    log(msg) {
        console.log("util.log: ", msg);
    }
};
複製代碼

在新的ts項目中,咱們須要使用這個工具庫

// a.ts
import util = require('./util.js') // 因爲util模塊使用commonjs規範,使用import = require語法導入模塊

// 此處編輯器不會幫咱們作任何提示
util.log("msg")
util.log() // 不知道參數的個數、類型等信息
複製代碼

因爲util是js文件,咱們沒法使用ts的類型推斷功能,也不知道util.log方法的參數類型和返回值類型。接下來讓咱們編寫util.d.ts來幫助編輯器和ts編譯器

// util.d.ts
declare var util: {
    test(msg: string): void;
};

export = util;
複製代碼

此時查看a.ts中的代碼(可能須要重啓下vscode),就能夠看見下面的錯誤提示

這樣,咱們就完成了在ts項目中爲js庫增長類型推斷的功能。

上面的例子展現了在現有ts項目中引入js模塊,並增長類型檢測的方法。若是想要了解更多關於d.ts的內容,能夠參考

另外上面這個例子也從側面展現了將現有js項目遷移到ts的方式。通常來講,將現有JavaScript項目遷移到TypeScript項目是十分簡單的,因爲任何JS文件都是有效的TS文件,所以最簡單的遷移流程應該是

  • 添加一個 tsconfig.json 文件,方便配置項目和編譯相關信息
  • 把文件擴展名從 .js 改爲 .ts,開始使用 any 來減小錯誤;
  • 開始在 TypeScript 中寫代碼,儘量的減小 any 的使用;
  • 回到舊代碼,開始添加類型註解,並修復已識別的錯誤;
  • 手動編寫d.ts文件,爲第三方 JavaScript 代碼定義環境聲明。

開發node服務

有了ts-node,搭建node服務開發環境就變得比較簡單了,結合nodemon,還能夠實現文件熱更新等功能。

export NODE_ENV=development && nodemon --watch 'server/**/*' -e ts,tsx --exec ts-node ./server/index.ts
複製代碼

參考

開發前端應用

vue

vue源碼中使用flow做爲類型檢測機制,在正在開發的vue3版本中,計劃改用typescript,所以在學習typescript並在vue開發中ts就變得理所應當,參考TypeScript 支持-Vue文檔

react

根據描述,typescript支持內嵌、類型檢查以及將JSX直接編譯爲js文件,所以在react中使用ts是十分方便的。參考TypeScript 中文手冊-React

小結

Typescript有下面幾個優勢

  • 在開發階段,若是參數類型不正確,或者調用了不存在的方法,就會在編譯階段拋出錯誤,減小潛在的bug
  • 強類型的變量和參數,容許IDE提供代碼智能提示,也方便代碼閱讀

本文主要整理了在學習Typescript過程當中的一些筆記

  • 介紹了ts開發環境的安裝,使用ts-node快速運行ts代碼
  • 整理了ts中的基本類型(number、boolean、enum等)和自定義類型(接口、類、類型別名)相關語法
  • 整理了ts中泛型和模塊的相關概念
  • .d.ts出發,瞭解了從JavaScript項目遷移到TypeScript的大體流程

固然還遺漏了不少細節語法等問題,須要在項目使用中進一步學習。最後放上一個問題:弱類型、強類型、動態類型、靜態類型語言的區別是什麼?

相關文章
相關標籤/搜索