4000字講清 《深刻理解TypeScript》一書 【基礎篇】

TypeScript,已經成爲前端避不開的基礎
前端

在讀完《深刻理解TypeScript》以後,寫下這篇總結

TypeScript解決的最關鍵痛點是什麼?

Type類型的約束、不肯定狀況下的提示、在代碼編寫階段就能知道本身的錯誤 node

這三點我認爲是最關鍵的點,自己TypeScript能作的事情,JavaScript都能作,雖然使用TS要多寫不少代碼,可是其實真正算下來,是能夠節省大量時間,由於你在編寫的時候就能知道哪裏有問題。jquery

呼籲你們,全面擁抱TypeScript ,TypeScript確定是將來

須要從JavaScript項目遷移:

假設:webpack

你知道 JavaScriptweb

你知道在項目中使用經常使用的方式和構建工具(如:webpack)。npm

有了以上假設,從 JavaScript 遷移,總的來講包括如下步驟:json

添加一個 tsconfig.json文件;安全

把文件擴展名從 .js 改爲 .ts,開始使用 any 來減小錯誤;微信

開始在 TypeScript 中寫代碼,儘量的減小 any 的使用;編輯器

回到舊代碼,開始添加類型註解,並修復已識別的錯誤;

爲你的第三方 JavaScript 代碼定義環境聲明。

記住全部的 JavaScript 都是有效的 TypeScript。這意味着,若是讓 TypeScript 編譯器編譯 TypeScript 裏的 JavaScript 代碼,編譯後的結果將會與原始的 JavaScript 代碼如出一轍。也就是說,把文件擴展名從 .js 改爲 .ts 將不會形成任何負面的影響。

第三方代碼

你能夠將你的 JavaScript 的代碼改爲 TypeScript 代碼,可是你不能讓這個世界都使用 TypeScript。這正是 TypeScript 環境聲明支持的地方。咱們建議你建立一個 vendor.d.ts 文件做爲開始(.d.ts 文件擴展名指定這個文件是一個聲明文件),而後咱們能夠向文件裏添加東西。或者,你也能夠建立一個針對於特定庫的聲明文件,如爲 jquery 建立 jquery.d.ts 文件。

幾乎排名前 90%JavaScript 庫的聲明文件存在於 DefinitelyTyped 這樣一個倉庫裏,在建立本身定義的聲明文件以前,咱們建議你先去倉庫中尋找。雖然建立一個聲明文件這種快速可是很差的方式是減少使用 TypeScript 初始阻力的重要步驟。

考慮使用 jquery 的用例,你能夠很是簡單快速的爲它建立一個定義:

declare var $: any;

有時候,你可能想給某些變量一些明確的定義(如:jquery),而且你會在類型聲明空間中使用它。你能夠經過 type 關鍵字快速的實現它:

declare type JQuery = any;
declare var $: JQuery;
這提供給你一個更清晰的使用模式。

再一次說明,一個高質量的 jquery.d.ts 已經在 DefinitelyTyped 中存在。如今你已經知道當你使用 JavaScript 第三方模塊時, 如何克服從 JavaScript TypeScript 的阻力。在接下去的內容,咱們將會討論環境聲明。

@types

你能夠經過 npm 來安裝使用 @types,以下例所示,你能夠爲 jquery 添加聲明文件:

npm install @types/jquery --save-dev

@types 支持全局和模塊類型定義

安裝完以後,不須要特別的配置,你就能夠像使用模塊同樣使用它:

import * as $ from 'jquery';

變量

舉個例子,當你想告訴 TypeScript 編輯器關於 process 變量時,你能夠這麼作:

declare let process: any

TIP

你並不須要爲 process 作這些,由於這已經存在於社區維護的 node.d.ts

這容許你使用 process,並能成功經過 TypeScript

process.exit();

推薦儘量的使用接口,例如:

interface Process {
  exit(code?: number): void;
}

declare let process: Process;

類實現接口:

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

class MyPoint implements Point {
  x: number;
  y: number; // Same as Point
}

枚舉

枚舉是組織收集有關聯變量的一種方式,其餘語言都有,因此TS中也加入了這個功能

enum CardSuit {
  Clubs,
  Diamonds,
  Hearts,
  Spades
}

// 簡單的使用枚舉類型
let Card = CardSuit.Clubs;

// 類型安全
Card = 'not a member of card suit'; // Error: string 不能賦值給 `CardSuit` 類型



enum Tristate {
  False,
  True,
  Unknown
}
編譯成 JavaScript:
var Tristate;
(function(Tristate) {
  Tristate[(Tristate['False'] = 0)] = 'False';
  Tristate[(Tristate['True'] = 1)] = 'True';
  Tristate[(Tristate['Unknown'] = 2)] = 'Unknown';
})(Tristate || (Tristate = {}));

這意味着咱們能夠跨文件、模塊拆分枚舉定義~

enum Color {
  Red,
  Green,
  Blue
}

enum Color {
  DarkRed = 3,
  DarkGreen,
  DarkBlue
}
TIP:你應該在枚舉的延續塊中,初始化第一個成員,以便生成的代碼不是先前定義的枚舉類型值。TypeScript 將會發出警告,若是你定義初始值

函數聲明:

type LongHand = {
  (a: number): number;
};

type ShortHand = (a: number) => number;

可調用的

interface ReturnString {
  (): string;
}

箭頭函數

const simple: (foo: number) => string = foo => foo.toString();

TIP

它僅僅只能作爲簡單的箭頭函數,你沒法使用重載。若是想使用它,你必須使用完整的 { (someArgs): someReturn } 的語法

可實例化:

interface CallMeWithNewToGetString {
  new (): string;
}
// 使用
declare const Foo: CallMeWithNewToGetString;
const bar = new Foo(); // bar 被推斷爲 string 類型

類型斷言:

推薦只使用統一的as foo 語法,而不是<foo>

初始用法:

let foo: any;
let bar = <string>foo; // 如今 bar 的類型是 'string'

然而,當你在 JSX 中使用 <foo> 的斷言語法時,這會與 JSX 的語法存在歧義:

let foo = <string>bar;</string>;

所以,爲了一致性,咱們建議你使用 as foo 的語法來爲類型斷言

類型斷言和類型轉換

它之因此不被稱爲「類型轉換」,是由於轉換一般意味着某種運行時的支持。可是,類型斷言純粹是一個編譯時語法,同時,它也是一種爲編譯器提供關於如何分析代碼的方法

類型斷言一般被認爲是有害的

在不少情景下,斷言能讓你更容易的從遺留項目中遷移(甚至將其餘代碼粘貼複製到你的項目中),然而,你應該當心謹慎的使用斷言。讓咱們用最初的代碼作爲示例,若是你沒有按約定添加屬性,TypeScript 編譯器並不會對此發出錯誤警告:

interface Foo {
  bar: number;
  bas: string;
}

const foo = {} as Foo;

// ahhh, 忘記了什麼?
上面的foo,並無bar和bas屬性,可是經過了檢驗。這是至關危險的,那熟悉的xx from undefined 報錯

雙重斷言

類型斷言,儘管咱們已經證實了它並非那麼安全,但它也仍是有用武之地。以下一個很是實用的例子所示,當使用者瞭解傳入參數更具體的類型時,類型斷言能按預期工做:

function handler(event: Event) {
  const mouseEvent = event as MouseEvent;
}

然而,以下例子中的代碼將會報錯,儘管使用者已經使用了類型斷言:

function handler(event: Event) {
  const element = event as HTMLElement; // Error: 'Event' 和 'HTMLElement' 中的任何一個都不能賦值給另一個
}

若是你仍然想使用那個類型,你可使用雙重斷言。首先斷言成兼容全部類型的 any,編譯器將不會報錯:

function handler(event: Event) {
  const element = (event as any) as HTMLElement; // ok
}

TypeScript 是怎麼肯定單個斷言是否足夠

S 類型是 T 類型的子集,或者 T 類型是 S 類型的子集時,S 能被成功斷言成 T。這是爲了在進行類型斷言時提供額外的安全性,徹底毫無根據的斷言是危險的,若是你想這麼作,你可使用 any

Freshness

爲了能讓檢查對象字面量類型更容易,TypeScript 提供 「Freshness」 的概念(它也被稱爲更嚴格的對象字面量檢查)用來確保對象字面量在結構上類型兼容。

結構類型很是方便。考慮以下例子代碼,它可讓你很是便利的從 JavaScript 遷移至 TypeScript,而且會提供類型安全:

function logName(something: { name: string }) {
  console.log(something.name);
}

const person = { name: 'matt', job: 'being awesome' };
const animal = { name: 'cow', diet: 'vegan, but has milk of own specie' };
const randow = { note: `I don't have a name property` };

logName(person); // ok
logName(animal); // ok
logName(randow); // Error: 沒有 `name` 屬性

可是,結構類型有一個缺點,它能誤導你認爲某些東西接收的數據比它實際的多。以下例,TypeScript 發出錯誤警告:

function logName(something: { name: string }) {
  console.log(something.name);
}

logName({ name: 'matt' }); // ok
logName({ name: 'matt', job: 'being awesome' }); // Error: 對象字面量只能指定已知屬性,`job` 屬性在這裏並不存在。
WARNING
請注意,這種錯誤提示,只會發生在對象字面量上

容許分配而外的屬性:

一個類型可以包含索引簽名,以明確代表可使用額外的屬性:

let x: { foo: number, [x: string]: any };
x = { foo: 1, baz: 2 }; // ok, 'baz' 屬性匹配於索引簽名

readonly在React中

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 是不可變的
    this.state.baz = 456; // Error: 你應該使用 this.setState()
  }
}

泛型

// 建立一個泛型類
class Queue<T> {
  private data: T[] = [];
  push = (item: T) => this.data.push(item);
  pop = (): T | undefined => this.data.shift();
}

// 簡單的使用
const queue = new Queue<number>();
queue.push(0);
queue.push('1'); // Error:不能推入一個 `string`,只有 number 類型被容許
你能夠隨意調用泛型參數,當你使用簡單的泛型時,泛型經常使用 T、U、V 表示。若是在你的參數裏,不止擁有一個泛型,你應該使用一個更語義化名稱,如 TKey TValue (一般狀況下,以 T 做爲泛型的前綴,在其餘語言如 C++ 裏,也被稱爲模板)

變體

對類型兼容性來講,變體是一個利於理解和重要的概念。

對一個簡單類型 BaseChild 來講,若是 Child Base 的子類,Child 的實例能被賦值給 Base 類型的變量。

Never

never 類型是 TypeScript 中的底層類型。它天然被分配的一些例子:

一個歷來不會有返回值的函數(如:若是函數內含有 while(true) {})

一個老是會拋出錯誤的函數(如:function foo() { throw new Error('Not Implemented') },foo 的返回類型是 never

你也能夠將它用作類型註解:

let foo: never; // ok
可是,never 類型僅能被賦值給另一個 never:
let foo: never = 123; // Error: number 類型不能賦值給 never 類型

// ok, 作爲函數返回類型的 never
let bar: never = (() => {
  throw new Error('Throw my hands in the air like I just dont care');
})();

與 void 的差別

一旦有人告訴你,never 表示一個歷來不會優雅的返回的函數時,你可能立刻就會想到與此相似的 void,然而實際上,void 表示沒有任何類型,never 表示永遠不存在的值的類型。

當一個函數沒有返回值時,它返回了一個 void 類型,可是,當一個函數根本就沒有返回值時(或者老是拋出錯誤),它返回了一個 never,void 指能夠被賦值的類型(在 strictNullCheckingfalse 時),可是 never 不能賦值給其餘任何類型,除了 never

TypeScript 索引簽名

JavaScript 在一個對象類型的索引簽名上會隱式調用 toString 方法,而在 TypeScript 中,爲防止初學者砸傷本身的腳(我老是看到 stackoverflow 上有不少 JavaScript 使用者都會這樣。),它將會拋出一個錯誤。

const obj = {
  toString() {
    return 'Hello';
  }
};

const foo: any = {};

// ERROR: 索引簽名必須爲 string, number....
foo[obj] = 'World';

// FIX: TypeScript 強制你必須明確這麼作:
foo[obj.toString()] = 'World';

聲明一個索引簽名

咱們經過使用 any 來讓 TypeScript 容許咱們能夠作任意咱們想作的事情。實際上,咱們能夠明確的指定索引簽名。例如:假設你想確認存儲在對象中任何內容都符合 { message: string } 的結構,你能夠經過 [index: string]: { message: string }來實現。

const foo: {
  [index: string]: { message: string };
} = {};

// 儲存的東西必須符合結構
// ok
foo['a'] = { message: 'some message' };

// Error, 必須包含 `message`
foo['a'] = { messages: 'some message' };

// 讀取時,也會有類型檢查
// ok
foo['a'].message;

// Error: messages 不存在
foo['a'].messages;

TIP

索引簽名的名稱(如:{ [index: string]: { message: string } } 裏的 index )除了可讀性外,並無任何意義。例如:若是有一個用戶名,你可使用 { username: string}: { message: string },這有利於下一個開發者理解你的代碼。
當你聲明一個索引簽名時,全部明確的成員都必須符合索引簽名:
// ok
interface Foo {
  [key: string]: number;
  x: number;
  y: number;
}

// Error
interface Bar {
  [key: string]: number;
  x: number;
  y: string; // Error: y 屬性必須爲 number 類型
}

至此,4000字簡單介紹了TypeScript的基礎內容部分,固然,這裏每一個部分均可以被拓展出來說好久。須要你們認真去看《深刻理解TypeScript

下一章,針對TypeScript的原理、工程化環境等進行進階編寫~

寫在最後:

以爲寫得不錯,歡迎關注微信公衆號:前端巔峯

回覆:進羣 便可加入小姐姐多多多多的大前端交流羣~

相關文章
相關標籤/搜索