TypeScript 簡介及編碼規範

  • 2019 年 11 月 5 日
  • 筆記

TypeScript 是什麼

TypeScript 是一種由微軟開發的自由和開源的程式語言。它是 JavaScript 的一個超集,而且本質上向這個語言添加了可選的靜態類型和基於類的面向對象編程。

TypeScript 擴展了 JavaScript 的句法,所以任何現有的 JavaScript 程式可以不加改變的在 TypeScript 下工作。TypeScript 是為大型應用之開發而設計,而編譯時它產生 JavaScript 以確保兼容性。

  • 安裝 TypeScript
$ npm install -g typescript
  • 編譯 TypeScript 文件
$ tsc app.ts # app.ts => app.js

TypeScript 基礎類型

Boolean 類型

let isDone: boolean = false; // tsc => var isDone = false;

Number 類型

let count: number = 10;  // tsc => var count = 10

String 類型

let name: string = 'Semliker'; // tsc => var name = 'Semlinker'

Array 類型

let list: number[] = [1,2,3]; // tsc => var list = [1,2,3];    let list: Array<number> = [1,2,3];  // tsc => var list = [1,2,3];

Enum 類型

enum Direction {  	NORTH,      SOUTH,      EAST,      WEST  };    let dir: Direction = Direction.NORTH;

默認情況下,NORTH 的初始值為 0,其餘的成員會從 1 開始自動增長。換句話說,Direction.SOUTH 的值為 1,Direction.EAST 的值為 2,Direction.WEST 的值為 3。

當然我們也可以設置 NORTH 的初始值,比如:

enum Direction {  	NORTH = 3,    SOUTH,    EAST,    WEST  };

在 TypeScript 2.4 版本,允許我們使用字元串枚舉。在一個字元串枚舉里,每個成員都必須用字元串字面量,或另外一個字元串枚舉成員進行初始化。

enum Direction {    NORTH = 'NORTH',    SOUTH = 'SOUTH',    EAST = 'EAST',    WEST = 'WEST',  }

Any (動態類型)

let notSure: any = 4;  notSure = "maybe a string instead";  notSure = false;    => tsc =>    var notSure = 4;  notSure = "maybe a string instead";  notSure = false;

Tuple

元組類型允許表示一個已知元素數量和類型的數組,各元素的類型不必相同。比如,你可以定義一對值分別為 stringnumber 類型的元組。

let x: [string, number];    x = ['semlinker', 10]; // 正常賦值    x = [10, 'semlinker']; // 類型不匹配

當訪問一個已知索引的元素,會得到正確的類型:

console.log(x[0].substr(1)); // OK    // Error, 'number' does not have 'substr' method  console.log(x[1].substr(1));

當訪問一個越界的元素,會使用聯合類型替代:

x[3] = 'world'; // OK, 字元串可以賦值給(string | number) 類型    console.log(x[5].toString()); // OK, 'string' 和 'number' 都有 toString 方法    x[6] = true; // Error, 布爾不是(string | number) 類型

Void

某種程度上來說,void 類型像是與 any 類型相反,它表示沒有任何類型。當一個函數沒有返回值時,你通常會見到其返回值類型是 void:

// 聲明函數返回值為void  function warnUser(): void {      console.log("This is my warning message");  }    => tsc =>    function warnUser() {  	console.log("This is my warning message");  }

需要注意的是,聲明一個 void 類型的變數沒有什麼作用,因為它的值只能為 undefinednull

let unusable: void = undefined;

Null and Undefined

TypeScript 里,undefinednull 兩者各自有自己的類型分別叫做 undefinednull

let u: undefined = undefined;  let n: null = null;

默認情況下 nullundefined 是所有類型的子類型。 就是說你可以把 nullundefined 賦值給 number 類型的變數。然而,當你指定了--strictNullChecks 標記,nullundefined 只能賦值給 void 和它們各自。

Never

never 類型表示的是那些永不存在的值的類型。 例如,never 類型是那些總是會拋出異常或根本就不會有返回值的函數表達式或箭頭函數表達式的返回值類型。

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

TypeScript Assertion

有時候你會遇到這樣的情況,你會比 TypeScript 更了解某個值的詳細資訊。通常這會發生在你清楚地知道一個實體具有比它現有類型更確切的類型。

通過類型斷言這種方式可以告訴編譯器,」相信我,我知道自己在幹什麼」。類型斷言好比其他語言里的類型轉換,但是不進行特殊的數據檢查和解構。它沒有運行時的影響,只是在編譯階段起作用。

類型斷言有兩種形式:

  • 「尖括弧」語法
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;

TypeScript Union Types and Type Aliases

Union Types

let greet = (message: string | string[]) => {    if(message instanceof Array) {      let messages = "";      message.forEach((msg) => {        messages += ` ${msg}`;      });      console.log("Received messages ", messages);    } else {      console.log("Received message = ", message);    }  };    greet('semlinker');  greet(['Hello', 'Angular']);

Type Aliases

type Message = string | string[];  let greet = (message: Message) => {    // ...  };

TypeScript Function

TypeScript 函數與 JavaScript 函數的區別

TypeScript

JavaScript

Types

No types

Arrow function

Arrow function (ES2015)

Function types

No Function types

Required and Optional parameters

All parameters are optional

Default parameters

Default parameters

Rest parameters

Rest parameters

Overloaded function

No overloaded functions

箭頭函數

  • 常見語法
myBooks.forEach(() => console.log('Done reading'));    myBooks.forEach(title => console.log(title));    myBooks.forEach((title, idx, arr) =>    console.log(idx + '-' + title);  );    myBooks.forEach((title, idx, arr) => {    console.log(idx + '-' + title);  });
  • 使用示例
// 未使用箭頭函數  function Book() {    let self = this;    self.publishDate = 2016;    setInterval(function() {      console.log(self.publishDate);    }, 1000);  }    // 使用箭頭函數  function Book() {    this.publishDate = 2016;    setInterval(() => {      console.log(this.publishDate);    }, 1000);  }

參數類型和返回類型

function createUserId(name: string, id: number): string {    return name + id;  }

函數類型

let IdGenerator: (chars: string, nums: number) => string;    function createUserId(name: string, id: number): string {    return name + id;  }    IdGenerator = createUserId;

可選參數及默認參數

// 可選參數  function createUserId(name: string, age?: number,    id: number): string {      return name + id;  }    // 默認參數  function createUserId(name: string = 'Semlinker', age?: number,    id: number): string {      return name + id;  }

剩餘參數

function push(array, ...items) {    items.forEach(function(item) {      array.push(item);    });  }    let a = [];  push(a, 1, 2, 3);

TypeScript Array

數組解構

let x: number, let y: number ,let z: number;  let five_array = [0,1,2,3,4];  [x,y,z] = five_array;

數組展開運算符

let two_array = [0,1];  let five_array = [...two_array,2,3,4];

數組循環

let colors: string[] = ["red", "green", "blue"];  for(let i in colors) {    console.log(i);  }

TypeScript Object

對象解構

let person = {    name: 'Semlinker',    gender: 'male'  };    let {name, gender} = person;

對象展開運算符

let person = {    name: 'Semlinker',    gender: 'male',    address: 'Xiamen'  };    // 組裝對象  let personWithAge = {...person, age: 31};    // 獲取除了某些項外的其它項  let {name, ...rest} = person;

TypeScript Interface

在面向對象語言中,介面(Interfaces)是一個很重要的概念,它是對行為的抽象,而具體如何行動需要由類(classes)去實現(implements)。

TypeScript 中的介面是一個非常靈活的概念,除了可用於對類的一部分行為進行抽象以外,也常用於對「對象的形狀(Shape)」進行描述。

對象的形狀

interface Person {    name: string;    age: number;  }    let semlinker: Person = {    name: 'Semlinker',    age: 31  };

可選 | 只讀屬性

interface Person {    readonly name: string;    age?: number;  }

只讀屬性用於限制只能在對象剛剛創建的時候修改其值。此外 TypeScript 還提供了 ReadonlyArray<T> 類型,它與 Array<T> 相似,只是把所有可變方法去掉了,因此可以確保數組創建後再也不能被修改。

let a: number[] = [1, 2, 3, 4];  let ro: ReadonlyArray<number> = a;  ro[0] = 12; // error!  ro.push(5); // error!  ro.length = 100; // error!  a = ro; // error!

TypeScript Class

在面向對象語言中,類是一種面向對象電腦程式語言的構造,是創建對象的藍圖,描述了所創建的對象共同的屬性和方法。

在 TypeScript 中,我們可以通過 Class 關鍵字來定義一個類:

class Greeter {     static cname: string = 'Greeter'; // 靜態屬性     greeting: string; // 成員屬行       constructor(message: string) { // 構造函數 - 執行初始化操作       this.greeting = message;     }        static getClassName() { // 靜態方法        return 'Class name is Greeter';      }        greet() { // 成員方法        return "Hello, " + this.greeting;      }  }    let greeter = new Greeter("world");

TypeScript Accessors

在 TypeScript 中,我們可以通過 gettersetter 方法來實現數據的封裝和有效性校驗,防止出現異常數據。

let passcode = "hello angular 5";    class Employee {      private _fullName: string;        get fullName(): string {          return this._fullName;      }        set fullName(newName: string) {          if (passcode && passcode == "hello angular 5") {              this._fullName = newName;          }          else {              console.log("Error: Unauthorized update of employee!");          }      }  }    let employee = new Employee();  employee.fullName = "Bob Smith";  if (employee.fullName) {      console.log(employee.fullName);  }

TypeScript Inheritance

繼承 (Inheritance) 是一種聯結類與類的層次模型。指的是一個類 (稱為子類、子介面) 繼承另外的一個類 (稱為父類、父介面) 的功能,並可以增加它自己的新功能的能力,繼承是類與類或者介面與介面之間最常見的關係;繼承是一種 is-a 關係。

在 TypeScripe 中,我們可以通過 extends 關鍵字來實現繼承:

class Animal {      name: string;      constructor(theName: string) { this.name = theName; }      move(distanceInMeters: number = 0) {          console.log(`${this.name} moved ${distanceInMeters}m.`);      }  }    class Snake extends Animal {      constructor(name: string) { super(name); }      move(distanceInMeters = 5) {          console.log("Slithering...");          super.move(distanceInMeters);      }  }    let sam = new Snake("Sammy the Python");  sam.move();

TypeScript Generics

泛型(Generics)是允許同一個函數接受不同類型參數的一種模板。相比於使用 any 類型,使用泛型來創建可復用的組件要更好,因為泛型會保留參數類型。

泛型介面

interface GenericIdentityFn<T> {      (arg: T): T;  }

泛型類

class GenericNumber<T> {      zeroValue: T;      add: (x: T, y: T) => T;  }    let myGenericNumber = new GenericNumber<number>();  myGenericNumber.zeroValue = 0;  myGenericNumber.add = function(x, y) { return x + y; };

使用示例

interface Hero { // Hero 介面      id: number;      name: string;  }    getHeroes(): Observable<Hero[]> {    return Observable.of([       { id: 1, name: 'Windstorm' },       { id: 13, name: 'Bombasto' },       { id: 15, name: 'Magneta' },       { id: 20, name: 'Tornado' }    ]);  }

上面 getHeroes(): Observable<Hero[]> 表示調用 getHeroes() 方法後返回的是一個 Observable 對象,<Hero[]> 用於表示該 Observable 對象的觀察者,將會收到的數據類型。示例中表示將會返回 <Hero[]> 英雄列表。

tsconfig.json 簡介

tsconfig.json 的作用

  • 用於標識 TypeScript 項目的根路徑;
  • 用於配置 TypeScript 編譯器;
  • 用於指定編譯的文件。

tsconfig.json 重要欄位

  • files – 設置要編譯的文件的名稱;
  • include – 設置需要進行編譯的文件,支援路徑模式匹配;
  • exclude – 設置無需進行編譯的文件,支援路徑模式匹配;
  • compilerOptions – 設置與編譯流程相關的選項。

compilerOptions 支援很多選項,常見的有 baseUrltargetbaseUrlmoduleResolutionlib 等。

tsconfig.json 示例

{    "compileOnSave": false,    "compilerOptions": {      "baseUrl": "./",      "outDir": "./dist/out-tsc",      "sourceMap": true,      "declaration": false,      "moduleResolution": "node",      "emitDecoratorMetadata": true,      "experimentalDecorators": true,      "target": "es5",      "typeRoots": [        "node_modules/@types"      ],      "lib": [        "es2017",        "dom"      ],      "paths": {        "ngx-example-library": [          "dist/ngx-example-library"        ]      }    },    "angularCompilerOptions": {      "preserveWhitespaces": false    }  }

(示例來源 – angular6-example-app

編碼規範

變數和函數

使用駝峰(camelCase)命名變數和函數名

Bad

var FooVar;  function BarFunc() { }

Good

var fooVar;  function barFunc() { }

使用帕斯卡(PascalCase)命名類名

Bad

class foo { }

Good

class Foo { }

使用帕斯卡(PascalCase)命名類成員與方法

Bad

class Foo {      Bar: number;      Baz() { }  }

Good

class Foo {      bar: number;      baz() { }  }

介面

  • 使用帕斯卡(PascalCase)命名介面
  • 使用駝峰(camelCase)命令成員
  • 誤使用 I 前綴

Bad

interface IFoo { }

Good

interface Foo { }

類型

  • 使用帕斯卡(PascalCase)命名
  • 使用駝峰(camelCase)命令成員

命名空間

使用帕斯卡(PascalCase)命名

Bad

namespace foo { }

Good

namespace Foo { }

枚舉

使用帕斯卡(PascalCase)命名枚舉

Bad

enum color { }

Good

enum Color { }

使用帕斯卡(PascalCase)命名枚舉成員

Bad

enum Color {      red  }

Good

enum Color {      Red  }

Null vs Undefined

最好不好顯式使用不可用的值

Bad

let foo = { x:123, y:undefined };

Good

let foo: { x:number, y?:number } = { x:123 };

通常使用 undefined( 而不是返回一個類似於 { valid:boolean, value?:Foo } 的對象 )

Bad

return null;

Good

return undefined;

參考 Node.js 回調函數 Error First 風格(若未發生異常,error 參數值設置為 null)

Bad

cb(undefined)

Good

cb(null)

避免使用值比較判斷對象是否為 null 或 undefined

Bad

if (error === null)

Good

if (error)

數組

聲明數組時使用 foos:Foo[] 而不是 foos:Array<Foo>,便於閱讀

類型 vs 介面

當你需要複合類型時,使用 type

type Foo = number | { someProperty: number }

當你需要繼承或實現時,使用 interface

interface Foo {    foo: string;  }    interface FooBar extends Foo {    bar: string;  }    class X implements FooBar {    foo: string;    bar: string;  }

風格指南

  1. 使用箭頭函數代替匿名函數表達式。
  2. 只要需要的時候才把箭頭函數的參數括起來。比如,(x) => x + x 是錯誤的,下面是正確的做法:
    • x => x + x
    • (x,y) => x + y
    • <T>(x: T, y: T) => x === y
  3. 總是使用 {} 把循環體和條件語句括起來。
  4. 小括弧里開始不要有空白。逗號,冒號,分號後要有一個空格。比如:
    • for (let i = 0, n = str.length; i < 10; i++) { }
    • if (x < 10) { }
    • function f(x: number, y: string): void { }
  5. 每個變數聲明語句只聲明一個變數 。比如:使用 let x = 1; var y = 2; 而不是 let x = 1, y = 2;)。
  6. 如果函數沒有返回值,最好使用 void