Typescript真香祕笈

本文由 IMWeb 首發於 IMWeb 社區網站 imweb.io。點擊閱讀原文查看 IMWeb 社區更多精彩文章。javascript

1. 前言

2018年Stack Overflow Developer的調研(https://insights.stackoverflow.com/survey/2018/)顯示,TypeScript已經成爲比JavaScript更受開發者喜好的編程語言了。html

圖片

以前我其實對於typescript沒有太多好感,主要是認爲其學習成本比較高,寫起代碼來還要多寫不少類型聲明,而且會受到靜態類型檢查的限制,很不自由,與javascript的設計哲學♂相悖。我相信有不少人也抱持着這樣的想法。前端

然而,最近因爲項目須要,學習並使用了一波typescript,結果。。。java

圖片

2. Typescript是什麼?

typescript,顧名思義,就是type + javascript,也就是加上了類型檢查的js。官方對於typescript的介紹也指出,typescript是javascript的超集。純粹的js語法,在typescript中是徹底兼容的。可是反過來,用typescript語法編寫的代碼,卻不能在瀏覽器或者Node環境下直接運行,由於typescript自己並非Ecmascript標準語法。node

3. 爲何要使用Typescript?

不少人堅持javascript而不肯使用typescript的一個很大緣由是認爲javascript的動態性高,基本不須要考慮類型,而使用typescript將會大大削弱編碼的自由度。但實際上,動態性並不老是那麼美好的。至少,如今javascript的動態性帶來了如下三方面的問題:react

  1. 代碼可讀性差,維護成本高。webpack

    所謂」動態一時爽,重構火葬場「。缺少類型聲明,對於本身很是熟悉的代碼而言,問題不大。可是若是對於新接手或者太長時間沒有接觸的代碼,理解代碼的時候須要自行腦補各類字段與類型,若是不幸項目規模比較龐大,也沒什麼註釋,那麼你的反應大概會是像這樣的:git

    圖片

    有了typescript,每一個變量類型與結構一目瞭然,根本無需自行腦補。搭配編輯器的智能提示,體驗可謂溫馨,媽媽不再用擔憂我拼錯字段名了。es6

  2. 缺少類型檢查,低級錯誤出現概率高。github

    人的專一力很難一直都保持高度在線狀態,若是沒有類型檢查,很容易出現一些低級錯誤。例如給某個string變量賦值數值,或給對象賦值時候缺乏了某些必要字段,調用函數時漏傳或者錯傳參數等。這些看起來很低級的錯誤,雖然大多數狀況下在自測或者測試階段,都能被驗出來,可是總會浪費你的一些時間去debug。

    使用typescript,這種狀況甚至不會發生,一旦你粗心地賦錯值,編輯器當即標紅提示,將bug扼殺在搖籃之中。

  3. 類型不肯定,運行時解析器須要進行類型推斷,存在性能問題。

    咱們知道javascript是邊解析邊執行的,因爲類型不肯定,因此同一句代碼可能須要被屢次編譯,這就形成性能上的開銷。

    雖然typescript如今沒法直接解決性能上的問題,由於typescript最終是編譯成javascript代碼的,可是如今已經有從typescript編譯到WebAssembly的工具了:https://github.com/AssemblyScript/assemblyscript。

好了,若是看完了上面的內容,您仍是表示對於typescript不感興趣,那麼後面的內容就能夠忽略了哈哈哈。。。

圖片

4. Typescript基礎篇

4.1 基礎類型

typescript中的基礎類型有:

圖片

其中,number、string、boolean、object、null、undefined、symbol都是比較簡單的。

例如:

let num: number = 1; // 聲明一個number類型的變量
let str: string = 'string'; // 聲明一個string類型的變量
let bool: boolean = true; // 聲明一個boolean類型的變量
let obj: object = { // 聲明一個object類型的變量
 a: 1,
}
let syb: symbol = Symbol(); // 聲明一個symbol類型的變量

null和undefined能夠賦值給除了never的其餘類型。

若是給變量賦予與其聲明類型不兼容的值,就會有報錯提示。

例如:

圖片

Array 數組類型

在typescript中,有兩種聲明數組類型的方式。

方式一:

let arr: Array<number> = [1, 2, 3]; // 聲明一個數組類型的變量

方式二:

let arr: number[] = [1, 2, 3]; // 聲明一個數組類型的變量

Tuple 元組類型

元組相似於數組,只不過元組元素的個數和類型都是肯定的。

let tuple: [number, boolean] = [0, false];

any類型

當不知道變量的類型時,能夠先將其設置爲any類型。

設置爲any類型後,至關於告訴typescript編譯器跳過這個變量的檢查,所以能夠訪問、設置這個變量的任何屬性,或者給這個變量賦任何值,編譯器都不會報錯。

let foo: any;
foo.test();
foo = 1;
foo = 'a';

void類型

一般用來聲明沒有返回值的函數的返回值類型。

function foo(): void {
}

never類型

一般用來聲明永遠不會正常返回的函數的返回值類型:

// 返回never的函數必須存在沒法達到的終點
function error(message: string): never {
 throw new Error(message);
}

// 返回never的函數必須存在沒法達到的終點
function infiniteLoop(): never {
 while (true) {
 }
}

never與void的區別即是,void代表函數會正常返回,可是返回值爲空。never表示的是函數永遠不會正常返回,因此不可能有值。

enum 枚舉類型

使用枚舉類型能夠爲一組數值賦予友好的名字。

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

默認狀況下,從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;

元素類型也支持字符串類型:

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

枚舉類型提供的一個便利是你能夠由枚舉的值獲得它的名字。例如,咱們知道數值爲2,可是不肯定它映射到Color裏的哪一個名字,咱們能夠查找相應的名字:

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

console.log(colorName);  // 顯示'Green'由於上面代碼裏它的值是2

4.2 類型斷言

有點相似其餘強類型語言的強制類型轉換,能夠將一個值斷言成某種類型,編譯器不會進行特殊的數據檢查和結構,因此須要本身確保斷言的準確性。

斷言有兩種形式,一種爲尖括號語法,一種爲as語法。

尖括號語法:

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

as語法:

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

在大部分狀況下,這兩種語法均可以使用,可是在jsx中就只能使用as語法了。

5. Typescript進階篇

5.1 函數

函數類型:

函數類型主要聲明的是參數和返回值的類型。

function sum(a: number, b: number): number {
 return a + b;
}

約等於

const sum: (numberA: number, numberB: number) => number = function(a: number, b: number): number {
 return a + b;
}

注意到類型定義時參數的名稱不必定要與實際函數的名稱一致,只要類型兼容便可。

可選參數:

函數參數默認都是必填的,咱們也可使用可選參數。

function sum(a: number, b: number, c?: number): number {
 return c ? a + b + c : a + b;
}

重載:

javascript自己是個動態語言。javascript裏函數根據傳入不一樣的參數而返回不一樣類型的數據是很常見的。

來看個簡單但沒什麼用的例子:

function doNothing(input: number): number;
function doNothing(input: string): string;
function doNothing(input): any {
 return input;
}

console.log(doNothing(123));
console.log(doNothing('123'));

固然也可使用聯合類型,可是編譯器就沒法準確知道返回值的具體類型。

function doNothing(input: number | string): number | string {
 return input;
}
console.log(doNothing('123').length); // 錯誤:Property 'length' does not exist on type 'string | number'

若是隻是單純參數的個數不一樣,返回值類型同樣,建議使用可選參數而不是重載。

function sum(a: number, b: number, c?: number) {
 return c ? a + b + c : a + b;
}

5.2 interface 接口

對於一些複雜的對象,須要經過接口來定義其類型。

interface SquareConfig {
 color: string;
 width: number;
}

const square: SquareConfig = {
 color: 'red', width: 0,
};

可選屬性:

默認狀況下,每一個屬性都是不能爲空的。若是這麼寫,將會有報錯。

interface SquareConfig {
 color: string;
 width: number;
}
const square: SquareConfig = {
 color: 'red',
};// error

能夠將用"?"將width標誌位可選的屬性:

interface SquareConfig {
 color: string;
 width?: number;
}
const square: SquareConfig = {
 color: 'red',
};

只讀屬性

一些對象屬性只能在對象剛剛建立的時候修改其值。你能夠在屬性名前用 readonly來指定只讀屬性。

interface Point {
   readonly x: number;
   readonly y: number;
}

若是在初始化後試圖修改只讀屬性的值,將會報錯。

let p: Point = { x: 10, y: 20 };
p.x = 20; // error

函數類型

接口除了可以描述對象的結構以外,還能描述函數的類型。

interface SumFunc {
 (a: number, b: number): number;
}

let sum: SumFunc;

sum = (numberA: number, numberB: number) => {
 return numberA + numberB;
}

能夠看到函數的類型與函數定義時只要參數類型一致便可,參數名不必定要同樣。

可索引類型

可索引類型,實際就是聲明對象的索引的類型,與對應值的類型。接口支持兩種索引類型,一種是number,一種是string,經過可索引類型能夠聲明一個數組類型。

interface StringArray {
 [index: number]: string;
}

let myArray: StringArray;
myArray = ["Bob", "Fred"];

let myStr: string = myArray[0];

5.3 class 類

typescript中的類是javascript中類的超集,因此若是你瞭解es6中的class的語法,也不難理解typescript中class的語法了。

這裏主要說下typescript的class和javascript的class的不一樣之處:

  • 只讀屬性

  • public、private、protected修飾符

  • 抽象類

  • 實現接口

只讀屬性

相似於接口中的只讀屬性,只能在類實例初始化的時候賦值。

class User {
 readonly name: string;
 constructor (theName: string) {
  this.name = theName;
 }
}

let user = new User('Handsome');
user.name = 'Handsomechan'; // 錯誤!name是隻讀的

public、private、protected修飾符:

public修飾符表示屬性是公開的,能夠經過實例去訪問該屬性。類屬性默認都是public屬性。

class Animal {
   constructor(public name: string) {}
}
const animal = new Animal('tom');
console.log(animal.name); // 'tom'

注意在類的構造函數參數前加上修飾符是一個語法糖,上面的寫法等價於:

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

private修飾符表示屬性是私有的,只有實例的方法才能訪問該屬性。

class Animal {
 getName(): string { return this.name }
 constructor(private name: string) {}
}
const animal = new Animal('tom');
console.log(animal.getName()); // 'tom'
console.log(animal.name); // Property 'name' is private and only accessible within class 'Animal'.

protected修飾符表示屬性是保護屬性,只有實例的方法和派生類中的實例方法才能訪問到。

class Animal {
 constructor(public name: string, protected age: number) {}
}

class Cat extends Animal {
 getAge = ():number => {
   return this.age;
 }
}

const cat = new Cat('tom', 1);
console.log(cat.getAge()); // 1

抽象類

抽象類作爲其它派生類的基類使用。它們通常不會直接被實例化。不一樣於接口,抽象類能夠包含成員的實現細節。 abstract關鍵字是用於定義抽象類和在抽象類內部定義抽象方法。

abstract class Animal {
   abstract makeSound(): void; // 抽象方法,必須在派生類中實現
   move(): void {
       console.log('roaming the earch...');
   }
}
class Sheep extends Animal {
 makeSound() {
   console.log('mie~');
 }
}
const animal = new Animal(); // 錯誤,抽象類不能直接實例化
const sheep = new Sheep();
sheep.makeSound();
sheep.move();

實現接口

類能夠實現一個接口,從而使得類知足這個接口的約束條件。

interface ClockInterface {
   currentTime: Date;
}

class Clock implements ClockInterface {
   currentTime: Date;
   constructor(h: number, m: number) { }
}

5.4 泛型

泛型在強類型語言中很常見,泛型支持在編寫代碼時候使用類型參數,而沒必要在一開始肯定某種特定的類型。這樣作的緣由有兩個:

  1. 有時候沒辦法在代碼被使用以前知道類型。

    例如咱們封裝了一個request函數,用來發起http請求,返回請求響應字段。

    咱們在實現request函數的時候,其實是不能知道響應字段有哪些內容的,由於這跟特定的請求相關。

    因此咱們將類型肯定的任務留給了調用者。

    // 簡單封裝了一個request函數
    async function request<T>(url: string): Promise<T> {
     try {
       const result = await fetch(url).then((response) => {
         return response.json();
       });
       return result;
     } catch (e) {
       console.log('request fail:', e);
       throw e;
     }
    }

    async function getUserInfo(userId: string): void {
     const userInfo = await request<{
       nickName: string;
       age: number;
     }>(`user_info?id=${userId}`)
     console.log(userInfo); // { nickName: 'xx', age: xx }
    }

    getUserInfo('123');
  1. 提升代碼的複用率。

    若是對於不一樣類型,代碼的操做都是同樣的,那麼可使用泛型來提升代碼的複用率。

    // 獲取數組或者字符串的長度
    function getLen<T extends Array<any> | string>(arg: T): number {
     return arg ? arg.length : 0;
    }

固然,您可能以爲這兩點在javascript中均可以輕易作到,根本不須要泛型。是的,泛型自己是搭配強類型食用更佳的,在弱類型下沒意義。

在typescript中,泛型有幾種打開方式:

泛型函數:

function someFunction<T>(arg: T) : T {
 return arg;
}
console.log(someFunction<number>(123)); // 123

泛型類型:

  • interface

    interface UserInfo<T> {
     id: T;
     age: number;
    }
    const userInfo: UserInfo<number> = {
     id: 123,
     age: 23,
    }
  • type

    type UserInfo<T> = { // 同上
     id: T;
     age: number;
    }
    const userInfo: UserInfo<string> = {
     id: '123',
     age: 123,
    }

泛型類:

class UserInfo<T> {
 constructor(private id: T, private age: number) {};
 getId(): T {
   return this.id;
 }
}

咱們也能夠給類型變量加上一些約束。

泛型約束

有時編譯器不能肯定泛型裏面有什麼屬性,就會出現報錯的狀況。

function logLength<T>(arg: T): T {
   console.log(arg.length);  // Error: T doesn't have .length
   return arg;
}

解決方法是加上泛型約束。

interface TypeWithLength {
 length: number,
}
function logLength<T extends TypeWithLength>(arg: T): T {
 console.log(arg.length);  // ok
 return arg;
}

6. Typescript高級篇

6.1 高級類型

交叉類型

交叉類型是將多個類型合併爲一個類型。

interface typeA {
 a?: number,
}

interface typeB {
 b?: number,
}

let value: typeA & typeB = {};

value.a = 1; // ok
value.b = 2; // ok

聯合類型

聯合類型表示變量屬於聯合類型中的某種類型,使用時須要先斷言一下。

interface TypeA {
 a?: number,
}

interface TypeB {
 b?: number,
}

const value: TypeA | TypeB = {};

(<TypeA>value).a = 1; // ok

6.2 類型別名 type

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

type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
...

6.3 is

is關鍵字一般組成類型謂詞,做爲函數的返回值。謂詞爲 parameterName is Type這種形式, parameterName必須是來自於當前函數簽名裏的一個參數名。

function isFish(pet: Fish | Bird): pet is Fish {
   return (<Fish>pet).swim !== undefined;
}

這樣的好處是當函數調用後,若是返回true,編譯器會將變量的類型鎖定爲那個具體的類型。

例如:

if (isFish(pet)) {
 pet.swim(); // 進入這裏,編譯器認爲petFish類型。
} else {
 pet.fly(); // 進入這裏,編譯器認爲petBird類型。
}

6.4 keyof

keyof索引類型查詢操做符

interface Person {
   name: string;
   age: number;
}
type IndexType = keyof Person; // 'name' | 'age'

這樣作的好處是使得編譯器可以檢查到動態屬性的類型。

function pick<T, K extends keyof T>(obj: T, keys: K[]): T[K][] {
 return keys.map(key => obj[key]);
}

console.log(pick(person, ['name', 'age'])); // [string, number]

6.5 聲明合併

爲何須要聲明合併呢?

咱們思考一下,在javascript中,一個對象是否是可能有多重身份。

例如說,一個函數,它能夠做爲一個普通函數執行,它也能夠是一個構造函數。同時,函數自己也是對象,它也能夠有本身的屬性。

因此這注定了typescript中的類型聲明可能存在的複雜性,須要進行聲明的合併。

合併接口

最簡單也最多見的聲明合併類型是接口合併。從根本上說,合併的機制是把雙方的成員放到一個同名的接口裏。

interface Box {
   height: number;
   width: number;
}

interface Box {
   scale: number;
}

let box: Box = {height: 5, width: 6, scale: 10};

接口的非函數的成員應該是惟一的。若是它們不是惟一的,那麼它們必須是相同的類型。若是兩個接口中同時聲明瞭同名的非函數成員且它們的類型不一樣,則編譯器會報錯。

對於函數成員,每一個同名函數聲明都會被當成這個函數的一個重載。同時須要注意,當接口 A與後來的接口 A合併時,後面的接口具備更高的優先級。

合併命名空間

Animals聲明合併示例:

namespace Animals {
   export class Zebra { }
}

namespace Animals {
   export interface Legged { numberOfLegs: number; }
   export class Dog { }
}

等同於:

namespace Animals {
   export interface Legged { numberOfLegs: number; }

   export class Zebra { }
   export class Dog { }
}

命名空間與類和函數和枚舉類型合併

類與命名空間的合併:

class Album {
   label: Album.AlbumLabel;
}
namespace Album {
   export class AlbumLabel { }
}

函數與命名空間的合併:

function buildLabel(name: string): string {
   return buildLabel.prefix + name + buildLabel.suffix;
}

namespace buildLabel {
   export let suffix = "";
   export let prefix = "Hello, ";
}

console.log(buildLabel("Sam Smith"));

此外,類與枚舉、命名空間與枚舉等合併也是能夠的,這裏再也不話下。

6.6 聲明文件

聲明文件一般是以.d.ts結尾的文件。

若是隻有ts、tsx文件,那麼其實不須要聲明文件。聲明文件通常是在用第三方庫的時候纔會用到,由於第三方庫都是js文件,加上聲明文件以後,ts的編譯器才能知道第三庫暴露的方法、屬性的類型。

聲明語法:

  • declare vardeclare letdeclare const聲明全局變量

    // src/jQuery.d.ts
    declare let jQuery: (selector: string) => any;
  • declare function 聲明全局方法

    declare function jQuery(selector: string): any;
  • declare class聲明全局類

    // src/Animal.d.ts
    declare class Animal {
       name: string;
       constructor(name: string);
       sayHi(): string;
    }
  • declare enum 聲明全局枚舉類型

    declare enum Directions {
       Up,
       Down,
       Left,
       Right
    }
  • declare namespace 聲明(含有子屬性的)全局變量

    // src/jQuery.d.ts

    declare namespace jQuery {
       function ajax(url: string, settings?: any): void;
    }
  • interfacetype聲明全局類型

    interface AjaxSettingsInterface {
     method?: 'GET' | 'POST'
     data?: any;
    }

    type AjaxSettingsType = {
     method?: 'GET' | 'POST'
     data?: any;
    }
  • export 導出變量

    在聲明文件中只要用到了export、import就會被視爲模塊聲明文件。模塊聲明文件中的declare關鍵字不能聲明全局變量

    // types/foo/index.d.ts

    export const name: string;
    export function getName(): string;
    export class Animal {
       constructor(name: string);
       sayHi(): string;
    }
    export enum Directions {
       Up,
       Down,
       Left,
       Right
    }
    export interface Options {
       data: any;
    }

    對應的導入和使用模塊應該是這樣:

    // src/index.ts

    import { name, getName, Animal, Directions, Options } from 'foo';

    console.log(name);
    let myName = getName();
    let cat = new Animal('Tom');
    let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];
    let options: Options = {
       data: {
           name: 'foo'
       }
    };
  • export namespace導出(含有子屬性的)對象

    // types/foo/index.d.ts

    export namespace foo {
       const name: string;
       namespace bar {
           function baz(): string;
       }
    }

    // src/index.ts

    import { foo } from 'foo';

    console.log(foo.name);
    foo.bar.baz();

  • export default ES6 默認導出

    // types/foo/index.d.ts

    export default function foo(): string;
    // src/index.ts

    import foo from 'foo';

    foo();
  • export = commonjs 導出模塊

    // 總體導出
    module.exports = foo;
    // 單個導出
    exports.bar = bar;

    在 ts 中,針對這種模塊導出,有多種方式能夠導入,第一種方式是 const ... = require

    // 總體導入
    const foo = require('foo');
    // 單個導入
    const bar = require('foo').bar;

    第二種方式是import ... from,注意針對總體導出,須要使用 import * as來導入:

    // 總體導入
    import * as foo from 'foo';
    // 單個導入
    import { bar } from 'foo';

    第三種方式是 import ... require,這也是 ts 官方推薦的方式:

    // 總體導入
    import foo = require('foo');
    // 單個導入
    import bar = foo.bar;
  • export as namespace 庫聲明全局變量

    既能夠經過 <script>標籤引入,又能夠經過 import導入的庫,稱爲 UMD 庫。相比於 npm 包的類型聲明文件,咱們須要額外聲明一個全局變量,爲了實現這種方式,ts 提供了一個新語法 export as namespace

    通常使用 export as namespace時,都是先有了 npm 包的聲明文件,再基於它添加一條 export as namespace語句,便可將聲明好的一個變量聲明爲全局變量。

    // types/foo/index.d.ts

    export as namespace foo;
    export = foo;

    declare function foo(): string;
    declare namespace foo {
       const bar: number;
    }
  • declare global 擴展全局變量

    使用 declare global能夠在 npm 包或者 UMD 庫的聲明文件中擴展全局變量的類型。

    // types/foo/index.d.ts

    declare global {
       interface String {
           prependHello(): string;
       }
    }

    export {};

    // src/index.ts

    'bar'.prependHello();
  • declare module擴展模塊

    若是是須要擴展原有模塊的話,須要在類型聲明文件中先引用原有模塊,再使用 declare module擴展

    // types/moment-plugin/index.d.ts

    import * as moment from 'moment';

    declare module 'moment' {
       export function foo(): moment.CalendarKey;
    }

    // src/index.ts

    import * as moment from 'moment';
    import 'moment-plugin';

    moment.foo();

7. 項目接入

  1. 對於全部的項目,接入ts的第一步就是安裝typescript包,typescript包中包含tsc編譯工具。

    npm i typescript -D

  2. 新建tsconfig.js文件,添加編譯配置。

    示例:

    {
     "compilerOptions": {
       "noImplicitAny": false,
       "target": "es5",
       "jsx": "react",
       "allowJs": true,
       "sourceMap": true,
       "outDir": "./out",
       "module": "commonjs",
       "baseUrl": "./src"
     },
     "include": ["./src/**/*"],
     "exclude": ["./out"]
    }

    include表示要編譯的文件所在的位置。

    exclude表示哪些位置的文件夾不須要進行編譯,能夠優化編譯速度。

    compilerOptions中能夠配置編譯選項。其中noImplicitAny表示是否禁止隱式聲明any,默認爲false。

    target表示要將ts代碼轉換成的ECMAScript目標版本。

    jsx可選preserve,react或者react-native。其中preserve表示生成的代碼中保留全部jsx標籤,react-native等同於preserve,react表示將jsx標籤轉換成React.createElement函數調用。

    allowJs表示是否容許編譯js文件,默認爲false。

    sourceMap表示是否生成sourceMap,默認false。

    outDir表示生成的目標文件所在的文件夾。

    module指定生成哪一個模塊系統的代碼。

    baseUrl表示解析非相對模塊名的基準目錄。

    詳細配置參數文檔請見:https://www.tslang.cn/docs/handbook/compiler-options.html

  3. 有了tsc和tsconfig,實際上就能將ts文件轉換爲js文件了。可是咱們在實際工程的開發中,通常不會直接用tsc,例如在前端項目中,咱們但願能與tsc能和webpack結合起來。在node服務端項目中,咱們但願修改文件以後,可以只編譯修改過的文件,而且重啓服務。下面我將分別介紹前端webpack項目和node項目中接入ts的方法:

    前端項目:

    好了,很是簡單就完成了webpack項目接入ts。

    node項目:

    在node項目中,能夠直接使用tsc編譯文件,而後重啓服務,可是這樣在開發階段顯然是很是低效的。

    能不能讓node直接執行ts文件呢?這樣結合nodemon,就能夠很簡單地作到修改文件後自動重啓服務的效果了。有了ts-node,問題不大!

    ts-node支持直接運行ts文件,就像用node直接運行js文件同樣。它的原理是對node進行了一層封裝,在require ts模塊的時候,先調用tsc將ts文件編譯成js文件,而後再用node執行。

    安裝ts-nodenpm i ts-node -D

    運行ts文件:npx ts-node script.ts

    因爲ts-node其實是在運行階段對於ts文件進行編譯的,因此通常不在生產環境中直接使用ts-node,而是用tsc直接編譯一遍,就不會有運行時的編譯開銷了。

    1. 安裝ts-loader: npm i ts-loader -D

    2. 在webpack.config.js中加入相關的配置項

      module.exports = {
       mode: "development",
       devtool: "inline-source-map", // 生成source-map
       entry: "./app.ts",
       output: {
         filename: "bundle.js"
       },
       resolve: {
         // 添加.ts.tsx爲可解析的文件後綴
         extensions: [".ts", ".tsx", ".js", ".jsx"]
       },
       module: {
         rules: [
           // 使用ts-loader解析.ts或者.tsx後綴的文件
           { test: /\.tsx?$/, loader: "ts-loader" }
         ]
       }
      };
  4. 配置eslint

    通過上面的配置以後,若是編譯報錯會在命令行中有提示,而且在vscode中會對出錯的代碼進行標紅。

    若是咱們想進一步對於代碼風格進行規範化約束,須要配置eslint。實際上有專門針對typescript的lint工具ts-lint,可是如今並不推薦使用了,由於爲了統一ts和js的開發體驗,tslint正在逐步地合併到eslint上(https://medium.com/palantir/tslint-in-2019-1a144c2317a9)。

    1. 安裝eslint相關依賴

      npm i eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin -D

      其中:

      eslint: js代碼檢測工具。 

      @typescript-eslint/parser: 將ts代碼解析成ESTree,能夠被eslint所識別。

      @typescript-eslint/eslint-plugin: 提供了typescript相關的eslint規則列表。

    2. 配置.eslintrc.js文件

      module.exports = {
       parser: '@typescript-eslint/parser', // 添加parser
       extends: [
         'eslint-config-imweb',
         'plugin:@typescript-eslint/recommended', // 引入@typescript-eslint/recommended規則列表
       ],
       plugins: ['@typescript-eslint'], // 添加插件
       rules: {
         'react/sort-comp': 0,
         'import/extensions': 0,
         'import/order': 0,
         'import/prefer-default-export': 0,
         'react/no-array-index-key': 1,
       },
      };
    3. 進行了以上的步驟後,發現vscode中仍是沒有將不符合規則的代碼標紅。這裏的緣由是,vscode默認不會對.ts,.tsx後綴的文件進行eslint檢查,須要配置一下。在vscode的setting.json文件中加入如下配置:

       "eslint.validate": [
           "javascript",
           "javascriptreact",
           {
               "language": "typescriptreact",
               "autoFix": true
           },
            {
               "language": "typescript",
               "autoFix": true
           }
       ]
  5. js項目遷移到ts 

對於新的項目,天然不用說,直接開搞。可是對於舊項目,怎麼遷移呢?

首先第一步仍是要先接入typescript,如前文所述。

接下來就有兩種選擇:

  1. 若是項目不大,或者下定決心而且有人力重構整個項目,那麼能夠將項目中的.js、.jsx文件的後綴改爲.ts、tsx。不出意外,這時編輯器會瘋狂報錯,耐心地一個個去解決它們吧。

  2. 若是項目很龐大,沒法一會兒所有重構,實際上也不妨礙使用ts。

    tsconfig.json文件中配置allowJs: true就能夠兼容js。

    對於項目中的js文件,有三種處理方式。

    1. 不作任何處理。

    2. 對文件進行改動時候,順手改爲ts文件重構掉。

    3. 給js文件附加.d.ts類型聲明文件,特別是一些通用的函數或者組件,這樣在ts文件中使用到這些函數或者組件時,編輯器會有隻能提示,tsc也會根據聲明文件中的類型進行校驗。

在ts文件中引入npm安裝的模塊,可能會出現報錯,這是由於tsc找不到該npm包中的類型定義文件,由於有些庫是將類型定義文件和源碼分離的。

    有三種方式解決這一問題:

  1. 若是該庫在@types命名空間下已經有可用的類型定義文件,直接用npm安裝便可,例如

    npm i @types/react -D

  2. 若是該庫在@types命名空間下沒有可用的類型定義文件,能夠本身寫一個,而後給該庫的做者提個PR。

  3. 本地建立一個全局的類型定義文件,例如global.d.ts

    declare module 'lib' {
     export const test: () => void;
    }

    而後在ts文件中就可使用lib模塊中的test方法了。

     import { test } from 'lib';
相關文章
相關標籤/搜索