TypeScript入門徹底指南(基礎篇)

本文來源於團隊內分享。TypeScript的官方文檔雖然較爲全面,但通讀下來卻要耗時很多;另外,TypeScript中文資料自己也比較缺少,本文可做爲準備嘗試TypeScript的同窗入門使用,涵蓋了上手TypeScript以前所須要的全部基礎知識。javascript

爲何JS須要類型檢查

TypeScript的設計目標在這裏能夠查看到,簡單歸納爲兩點:前端

  1. 爲JavaScript提供一個可選擇的類型檢查系統;
  2. 爲JavaScript提供一個包含未來新特性的版本。

TypeScript的核心價值體如今第一點,第二點能夠認爲是TypeScript的向後兼容性保證,也是TypeScript必需要作到的。java

那麼爲何JS須要作靜態類型檢查呢?在幾年前這個問題也許還會存在比較大的爭議,在前端日趨複雜的今天,通過像Google、Microsoft、FaceBook這樣的大公司實踐代表,類型檢查對於代碼可維護性和可讀性是有很是大的幫助的,尤爲針對於須要長期維護的規模性系統。react

TypeScript優點

在我看來,TypeScript可以帶來最直觀上的好處有三點:git

  1. 幫助更好地重構代碼;
  2. 類型聲明自己是最好查閱的文檔。
  3. 編輯器的智能提示更加友好。

一個好的代碼習慣是時常對本身寫過的代碼進行小的重構,讓代碼往更可維護的方向去發展。然而對於已經上線的業務代碼,每每測試覆蓋率不會很高,當咱們想要重構時,常常會擔憂本身的改動會產生各類不可預知的bug。哪怕是一個小的重命名,也有可能照顧不到全部的調用處形成問題。github

若是是一個TypeScript項目,這種擔憂就會大大下降,咱們能夠依賴於TypeScript的靜態檢查特性幫助找出一個小的改動(如重命名)帶來的其餘模塊的問題,甚至對於模塊文件來講,咱們能夠直接藉助編輯器的能力進行「一鍵重命名」操做。ajax

另一個問題,若是你接手過一個老項目,確定會頭痛於各類文檔的缺失和幾乎沒有註釋的代碼,一個好的TypeScript項目,是能夠作到代碼即文檔的,經過聲明文件咱們能夠很好地看出各個字段的含義以及哪些是前端必須字段:算法

// 砍價用戶信息
export interface BargainJoinData {
  curr_price: number; // 當前價
  curr_ts: number; // 當前時間
  init_ts: number; // 建立時間
  is_bottom_price: number; // 砍到底價
}
複製代碼

TypeScript對開發者是友好的

TypeScript在設計之初,就肯定了他們的目標並非要作多麼嚴格完備的類型強校驗系統,而是可以更好地兼容JS,更貼合JS開發者的開發習慣。能夠說這是MS的商業戰略,也是TS可以成功的關鍵性因素之一。它對JS的兼容性主要表現爲如下三個方面:typescript

隱式的類型推斷

var foo = 123;
foo = "456"; // Error: cannot assign `string` to `number`
複製代碼

當咱們對一個變量或函數等進行賦值時,TypeScript可以自動推斷類型賦予變量,TypeScript背後有很是強大的自推斷算法幫助識別類型,這個特性無疑能夠幫助咱們簡化一些聲明,沒必要像強類型的語言那樣到處是聲明,也可讓咱們看代碼時更加輕鬆。設計模式

結構化的類型

TypeScript旨在讓JS開發者更簡單地上手,所以將類型設計爲「結構化」(Structural)的而非「名義式」(Nominal)的。

什麼意思呢?意味着TypeScript的類型並不根據定義的名字綁定,只要是形似的類型,無論名稱相不相同,均可以做爲兼容類型(這很像所謂的duck typing),也就是說,下面的代碼在TypeScript中是徹底合法的:

class Foo { method(input: string) { /* ... */ } }
class Bar { method(input: string) { /* ... */ } }
let test: Foo = new Bar(); // no Error!
複製代碼

這樣實際上能夠作到類型的最大化複用,只要形似,對於開發者也是最好理解的。(固然對於這個示例最好的作法是抽出一個公共的interface)

知名的JS庫支持

TypeScript有強大的DefinitelyTyped社區支持,目前類型聲明文件基本上已經覆蓋了90%以上的經常使用JS庫,在編寫代碼時咱們的提示是很是友好的,也能作到安全的類型檢查。(在使用第三方庫時,能夠如今這個項目中檢索一下有沒有該庫的TS聲明,直接引入便可)

回顧兩個基礎知識

在進入正式的TS類型介紹以前,讓咱們先回顧一下JS的兩個基礎:

相等性判斷

咱們都知道,在JS裏,兩個等號的判斷會進行隱式的類型轉換,如:

console.log(5 == "5"); // true 
console.log(0 == ""); // true
複製代碼

在TS中,由於有了類型聲明,所以這兩個結果在TS的類型系統中恆爲false,所以會有報錯:

This condition will always return 'false' since the types '5' and '"5"' have no overlap.
複製代碼

因此在代碼層面,一方面咱們要避免這樣兩個不一樣類型的比較,另外一方面使用全等來代替兩個等號,保證在編譯期和運行期具備相同的語義。

對於TypeScript而言,只有nullundefined的隱式轉換是合理的:

console.log(undefined == undefined); // true
console.log(null == undefined); // true
console.log(0 == undefined); // false
console.log('' == undefined); // false
console.log(false == undefined); // false
複製代碼

類(Class)

對於ES6的Class,咱們自己已經很熟悉了,值得一提的是,目前對於類的靜態屬性、成員屬性等有一個提案——proposal-class-fields已經進入了Stage3,這個提案包含了不少東西,主要是類的靜態屬性、成員屬性、公有屬性和私有屬性。其中,私有屬性的提案在社區內引發了很是大的爭議,因爲它的醜陋和怪異遭受各路人馬的抨擊,現TC39委員會已決定從新思考該提案。

如今讓咱們來看看TypeScript對屬性訪問控制的狀況:

可訪問性 public protected private
類自己
子類
類的實例

能夠看到,TS中的類成員訪問和其餘語言很是相似:

class FooBase {
    public x: number;
    private y: number;
    protected z: number;
}
複製代碼

對於類的成員構造函數初始化,TS提供了一個簡單的聲明方式:

class Foo {
    constructor(public x:number) {
    }
}
複製代碼

這段代碼和下面是等同的:

class Foo {
    x: number;
    constructor(x:number) {
        this.x = x;
    }
}
複製代碼

TS類型系統基礎

基本性準則

在正式瞭解TypeScript以前,首先要明確兩個基本概念:

  1. TypeScript的類型系統設計是可選的,意味着JavaScript就是TypeScript。
  2. TypeScript的報錯並不會阻止JS代碼的生成,你能夠漸進式地將JS逐步遷移爲TS。

基本語法

:<TypeAnnotation>
複製代碼

TypeScript的基本類型語法是在變量以後使用冒號進行類型標識,這種語法也揭示了TypeScript的類型聲明其實是可選的。

原始值類型

var num: number;
var str: string;
var bool: boolean;
複製代碼

TypeScript支持三種原始值類型的聲明,分別是numberstringboolean

對於這三種原始值,TS一樣支持以它們的字面量爲類型:

var num: 123;
var str: '123';
var bool: true;
複製代碼

這類字面量類型配合上聯合類型仍是十分有用的,咱們後面再講。

數組類型

對於數組的聲明也很是簡單,只須要加上一個中括號聲明類型便可:

var boolArray: boolean[];
複製代碼

以上就簡單地定義了一個布爾類型的數組,大多數狀況下,咱們數組的元素類型是固定的,若是咱們數組內存在不一樣類型的元素怎麼辦?

若是元素的個數是已知有限的,可使用TS的元組類型:

var nameNumber: [string, number];
複製代碼

該聲明也很是的形象直觀,若是元素個數不固定且類型未知,這種狀況較爲罕見,可直接聲明成any類型:

var arr: any[]
複製代碼

接口類型

接口類型是TypeScript中最多見的組合類型,它可以將不一樣類型的字段組合在一塊兒造成一個新的類型,這對於JS中的對象聲明是十分友好的:

interface Name {
    first: string;
    second: string;
}

var personName:Name = {
    first: '張三'
} // Property 'second' is missing in type '{ first: string; }' but required in type 'Name'
複製代碼

上述例子可見,TypeScript對每個字段都作了檢查,若未定義接口聲明的字段(非可選),則檢查會拋出錯誤。

內聯接口

對於對象來講,咱們也可使用內聯接口來快速聲明類型:

var personName:{ first: string, second: string } = {
    first: '張三'
} // Property 'second' is missing in type '{ first: string; }' but required in type 'Name'
複製代碼

內聯接口能夠幫助咱們快速聲明類型,但建議謹慎使用,對於可複用以及通常性的接口聲明建議使用interface聲明。

索引類型

對於對象而言,咱們可使用中括號的方式去存取值,對TS而言,一樣支持相應的索引類型:

interface Foo {
  [key:string]: number
}
複製代碼

對於索引的key類型,TypeScript只支持numberstring兩種類型,且Number是string的一種特殊狀況。

對於索引類型,咱們在通常化的使用場景上更方便:

interface NestedCSS {
  color?: string;
  nest?: {
    [selector: string]: NestedCSS;
  }
}

const example: NestedCSS = {
  color: 'red',
  nest: {
    '.subclass': {
      color: 'blue'
    }
  }
}
複製代碼

類的接口

對於接口而言,另外一個重要做用就是類能夠實現接口:

interface Point {
    x: number; y: number;
    z: number; // New member
}

class MyPoint implements Point { // ERROR : missing member `z`
    x: number; y: number;
}
複製代碼

對類而言,實現接口,意味着須要實現接口的全部屬性和方法,這和其餘語言是相似的。

函數類型

函數是TypeScript中最多見的組成單元:

interface Foo {
    foo: string;
}

// Return type annotated as `: Foo`
function foo(sample: Foo): Foo {
    return sample;
}
複製代碼

對於函數而言,自己有參數類型和返回值類型,均可進行聲明。

可選參數

對於參數,咱們能夠聲明可選參數,即在聲明以後加一個問號:

function foo(bar: number, bas?: string): void {
    // ..
}
複製代碼

void和never類型

另外,上述例子也代表,當函數沒有返回值時,能夠用void來表示。

當一個函數永遠不會返回時,咱們能夠聲明返回值類型爲never

function bar(): never {
    throw new Error('never reach');
}
複製代碼

callable和newable

咱們還可使用接口來定義函數,在這種函數實現接口的情形下,咱們稱這種定義爲callable:

interface Complex {
  (bar?: number, ...others: boolean[]): number;
}

var foo: Complex;
複製代碼

這種定義方式在可複用的函數聲明中很是有用。

callable還有一種特殊的狀況,該聲明中指定了new的方法名,稱之爲newable

interface CallMeWithNewToGetString {
  new(): string
}

var foo: CallMeWithNewToGetString;

new foo();
複製代碼

這個在構造函數的聲明時很是有用。

函數重載

最後,一個函數能夠支持多種傳參形式,這時候僅僅使用可選參數的約束多是不夠的,如:

unction padding(a: number, b?: number, c?: number, d?: number) {
    if (b === undefined && c === undefined && d === undefined) {
        b = c = d = a;
    }
    else if (c === undefined && d === undefined) {
        c = a;
        d = b;
    }
    return {
        top: a,
        right: b,
        bottom: c,
        left: d
    };
}
複製代碼

這個函數能夠支持四個參數、兩個參數和一個參數,若是咱們粗略的將後三個參數都設置爲可選參數,那麼當傳入三個參數時,TS也會認爲它是合法的,此時就失去了類型安全,更好的方式是聲明函數重載:

function padding(all: number);
function padding(topAndBottom: number, leftAndRight: number);
function padding(top: number, right: number, bottom: number, left: number);
function padding(a: number, b?: number, c?: number, d?: number) {
   //...
}
複製代碼

函數重載寫法也很是簡單,就是重複聲明不一樣參數的函數類型,最後一個聲明包含了兼容全部重載聲明的實現。這樣,TS類型系統就能準確的判斷出該函數的多態性質了。

使用callable的方式也能夠聲明重載:

interface Padding {
  (all: number): any
  (topAndBottom: number, leftAndRight: number): any
  (top: number, right: number, bottom: number, left: number): any
}
複製代碼

特殊類型

any

any在TypeScript中是一個比較特殊的類型,聲明爲any類型的變量就像動態語言同樣不受約束,好像關閉了TS的類型檢查通常。對於any類型的變量,能夠將其賦予任何類型的值:

var power: any;

power = '123';
power = 123;
複製代碼

any對於JS代碼的遷移是十分友好的,在已經成型的TypeScript項目中,咱們要慎用any類型,當你設置爲any時,意味着告訴編輯器不要對它進行任何檢查。

null和undefined

nullundefined做爲TypeScript的特殊類型,它一樣有字面量的含義,以前咱們已經瞭解到。

值得注意的是,nullundefined能夠賦值給任意類型的變量:

var num: number;
var str: string;

// 賦值給任意類型的變量都是合法的
num = null;
str = undefined;
複製代碼

void和never

在函數類型中,咱們已經介紹了兩種類型,專門修飾函數返回值。

readonly

readonly是隻讀屬性的修飾符,當咱們的屬性是隻讀時,能夠用該修飾符加以約束,在類中,用readonly修飾的屬性僅能夠在構造函數中初始化:

class Foo {
    readonly bar = 1; // OK
    readonly baz: string;
    constructor() {
        this.baz = "hello"; // OK
    }
}
複製代碼

一個實用場景是在react中,propsstate都是隻讀的:

interface Props {
    readonly foo: number;
}
interface State {
    readonly bar: number;
}
export class Something extends React.Component<Props,State> {
  someMethod() {
    this.props.foo = 123; // ERROR: (props are immutable)
    this.state.baz = 456; // ERROR: (one should use this.setState) 
  }
}
複製代碼

固然,React自己在類的聲明時會對傳入的propsstate作一層ReadOnly的包裹,所以不管咱們是否在外面顯式聲明,賦值給propsstate的行爲都是會報錯的。

注意,readonly聽起來和const有點像,須要時刻保持一個概念:

  • readonly是修飾屬性的
  • const是聲明變量的

泛型

在更加通常化的場景,咱們的類型可能並不固定已知,它和any有點像,只不過咱們但願在any的基礎上可以有更近一步的約束,好比:

function reverse<T>(items: T[]): T[] {
    var toreturn = [];
    for (let i = items.length - 1; i >= 0; i--) {
        toreturn.push(items[i]);
    }
    return toreturn;
}
複製代碼

reverse函數是一個很好的示例,對於一個通用的函數reverse來講,數組元素的類型是未知的,能夠是任意類型,但reverse函數的返回值也是個數組,它和傳入的數組類型是相同的,對於這個約束,咱們可使用泛型,其語法是尖括號,內置泛型變量,多個泛型變量用逗號隔開,泛型變量名稱沒有限制,通常而言咱們以大寫字母開頭,多個泛型變量使用其語義命名,加上T爲前綴。

在調用時,能夠顯示的指定泛型類型:

var reversed = reverse<number>([1, 2, 3]);
複製代碼

也能夠利用TypeScript的類型推斷,進行隱式調用:

var reversed = reverse([1, 2, 3]);
複製代碼

因爲咱們的參數類型是T[],而傳入的數組類型是一個number[],此時T的類型被TypeScript自動推斷爲number

對於泛型而言,咱們一樣能夠做用於接口和類:

interface Array<T> {
 reverse(): T[];
 // ...
}
複製代碼

聯合類型

在JS中,一個變量的類型可能擁有多個,好比:

function formatCommandline(command: string[]|string) {
    var line = '';
    if (typeof command === 'string') {
        line = command.trim();
    } else {
        line = command.join(' ').trim();
    }
}
複製代碼

此時咱們可使用一個|分割符來分割多種類型,對於這種複合類型,咱們稱之爲聯合類型

交叉類型

若是說聯合類型的語義等同於或者,那麼交叉類型的語義等同於集合中的並集,下面的extend函數是最好的說明:

function extend<T, U>(first: T, second: U): T & U {
    let result = <T & U> {};
    for (let id in first) {
        result[id] = first[id];
    }
    for (let id in second) {
        if (!result.hasOwnProperty(id)) {
            result[id] = second[id];
        }
    }
    return result;
}
複製代碼

該函數最終以T&U做爲返回值值,該類型既包含了T的字段,也包含了U的字段,能夠看作是兩個類型的並集

類型別名

TypeScript爲類型的複用提供了更便捷的方式——類型別名。當你想複用類型時,可能在該場景下要爲已經聲明的類型換一個名字,此時可使用type關鍵字來進行類型別名的定義:

interface state {
  a: 1
}

export type userState = state;
複製代碼

咱們一樣可使用type來聲明一個類型:

type Text = string | { text: string };
type Coordinates = [number, number];
type Callback = (data: string) => void;
複製代碼

對於type和interface的取捨:

  • 若是要用交叉類型或聯合類型,使用type。
  • 若是要用extend或implement,使用interface。
  • 其他狀況可看我的喜愛,我的建議type更多應當用於須要起別名時,其餘狀況儘可能使用interface。

枚舉類型

對於組織一系列相關值的集合,最好的方式應當是枚舉,好比一系列狀態集合,一系列歸類集合等等。

在TypeScript中,枚舉的方式很是簡單:

enum Color {
    Red,
    Green,
    Blue
}
var col = Color.Red;
複製代碼

默認的枚舉值是從0開始,如上述代碼,Red=0Green=1依次類推。

固然咱們還能夠指定初始值:

enum Color {
    Red = 3,
    Green,
    Blue
}
複製代碼

此時Red=3, Green=4依次類推。

你們知道在JavaScript中是不存在枚舉類型的,那麼TypeScript的枚舉最終轉換爲JavaScript是什麼樣呢?

var Color;
(function (Color) {
    Color[Color["Red"] = 0] = "Red";
    Color[Color["Green"] = 1] = "Green";
    Color[Color["Blue"] = 2] = "Blue";
})(Color || (Color = {}));
複製代碼

從編譯後的代碼能夠看到,轉換爲一個key-value的對象後,咱們的訪問也很是方便:

var red = Color.Red; // 0
var redKey = Color[0]; // 'Red'
var redKey = Color[Color.Red]; // 'Red'
複製代碼

既能夠經過key來訪問到值,也能夠經過值來訪問到key。

Flag標識位

對於枚舉,有一種很實用的設計模式是使用位運算來標識(Flag)狀態:

enum EnvFlags {
  None = 0,
  QQ = 1 << 0,
  Weixin = 1 << 1
}

function initShare(flags: EnvFlags) {
  if (flags & EnvFlags.QQ) {
    initQQShare();
  }
  if (flags & EnvFlags.Weixin) {
    initWeixinShare();
  }
}
複製代碼

在咱們使用標識位時,能夠遵循如下規則:

  • 使用 |= 增長標誌位
  • 使用 &=~清除標誌位
  • 使用 | 聯合標識位

如:

var flag = EnvFlags.None;
flag |= EnvFlags.QQ;    // 加入QQ標識位
Flag &= ~EnvFlags.QQ;   // 清除QQ標識位
Flag |=  EnvFlags.QQ | EnvFlags.Weixin; // 加入QQ和微信標識位
複製代碼

常量枚舉

在枚舉定義加上const聲明,便可定義一個常量枚舉:

enum Color {
    Red = 3,
    Green,
    Blue
}
複製代碼

對於常量枚舉,TypeScript在編譯後不會產生任何運行時代碼,所以在通常狀況下,應當優先使用常量枚舉,減小沒必要要代碼的產生。

字符串枚舉

TypeScript還支持非數字類型的枚舉——字符串枚舉

export enum EvidenceTypeEnum {
  UNKNOWN = '',
  PASSPORT_VISA = 'passport_visa',
  PASSPORT = 'passport',
  SIGHTED_STUDENT_CARD = 'sighted_tertiary_edu_id',
  SIGHTED_KEYPASS_CARD = 'sighted_keypass_card',
  SIGHTED_PROOF_OF_AGE_CARD = 'sighted_proof_of_age_card',
}
複製代碼

這類枚舉和咱們以前使用JavaScript定義常量集合的方式很像,好處在於調試或日誌輸出時,字符串比數字要包含更多的語義。

命名空間

在沒有模塊化的時代,咱們爲了防止全局的命名衝突,常常會以命名空間的形式組織代碼:

(function(something) {

    something.foo = 123;

})(something || (something = {}))
複製代碼

TypeScript內置了namespace變量幫助定義命名空間:

namespace Utility {
    export function log(msg) {
        console.log(msg);
    }
    export function error(msg) {
        console.error(msg);
    }
}
複製代碼

對於咱們本身的工程項目而言,通常建議使用ES6模塊的方式去組織代碼,而命名空間的模式可適用於對一些全局庫的聲明,如jQuery:

namespace $ {
  export function ajax(//...) {} } 複製代碼

固然,命名空間還能夠便捷地幫助咱們聲明靜態方法,如和enum的結合使用:

enum Weekday {
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday,
    Sunday
}
namespace Weekday {
    export function isBusinessDay(day: Weekday) {
        switch (day) {
            case Weekday.Saturday:
            case Weekday.Sunday:
                return false;
            default:
                return true;
        }
    }
}

const mon = Weekday.Monday;
const sun = Weekday.Sunday;
console.log(Weekday.isBusinessDay(mon)); // true
console.log(Weekday.isBusinessDay(sun)); // false
複製代碼

關於命名規範

變量名、函數和文件名

  • 推薦使用駝峯命名。
// Bad
var FooVar;
function BarFunc() { }

// Good
var fooVar;
function barFunc() { }
複製代碼

類、命名空間

  • 推薦使用帕斯卡命名。
  • 成員變量和方法推薦使用駝峯命名。
// Bad
class foo { }

// Good
class Foo { }

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

// Good
class Foo {
    bar: number;
    baz() { }
}
複製代碼

Interface、type

  • 推薦使用帕斯卡命名。
  • 成員字段推薦使用駝峯命名。
// Bad
interface foo { }

// Good
interface Foo { }

// Bad
interface Foo {
    Bar: number;
}

// Good
interface Foo {
    bar: number;
}
複製代碼

關於模塊規範

export default的爭論

關因而否應該使用export default這裏有詳盡的討論,在AirBnb規範中也有prefer-default-export這條規則,但我認爲在TypeScript中應當儘可能不使用export default

關於連接中提到的重命名問題, 甚至自動import,其實export default也是能夠作到的,藉助編輯器和TypeScript的靜態能力。因此這一點還不是關鍵因素。

不過使用通常化的export更讓咱們容易得到智能提示:

import /* here */ from 'something';
複製代碼

在這種狀況下,通常編輯器是不會給出智能提示的。 而這種:

import { /* here */ } from 'something';
複製代碼

咱們能夠經過智能提示作到快速引入。

除了這一點外,還有如下幾點好處:

  • 對CommonJS是友好的,若是使用export default,在commonJS下須要這樣引入:
const {default} = require('module/foo');
複製代碼

多了個default無疑感受很是奇怪。

  • 對動態import是友好的,若是使用export default,還須要顯示的經過default字段來訪問:
const HighChart = await import('https://code.highcharts.com/js/es-modules/masters/highcharts.src.js');
Highcharts.default.chart('container', { ... }); // 注意 `.default`
複製代碼
  • 對於re-exporting是友好的,若是使用export default,那麼進行re-export會比較麻煩:
import Foo from "./foo"; export { Foo }
複製代碼

相比之下,若是沒有export default,咱們能夠直接使用:

export * from "./foo"
複製代碼

實踐中的一些坑

實踐篇即將到來,敬請期待~

相關文章
相關標籤/搜索